From 6280b71236f095fb24bd2d6a4a7c7ce0bba1d97f Mon Sep 17 00:00:00 2001 From: Alex Schroeder Date: Tue, 16 Mar 2021 22:52:03 +0100 Subject: [PATCH 01/87] Typos in various README files fixed Also upcased acronyms such as URL, HTTP, HTTPS, CSS, and JSON; capitalised names such as Etherpad. --- README.md | 4 ++-- README_commandline.md | 28 ++++++++++++++-------------- README_customizations.md | 2 +- README_goals.md | 18 +++++++++--------- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 6c2c41ea5..fc9fa0755 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ -Epicyon is a modern [ActivityPub](https://www.w3.org/TR/activitypub) compliant server implementing both S2S and C2S protocols and sutable for installation on single board computers. It includes features such as moderation tools, post expiry, content warnings, image descriptions, news feed and perimeter defense against adversaries. It contains *no javascript* and uses HTML+CSS with a Python backend. +Epicyon is a modern [ActivityPub](https://www.w3.org/TR/activitypub) compliant server implementing both S2S and C2S protocols and suitable for installation on single board computers. It includes features such as moderation tools, post expiry, content warnings, image descriptions, news feed and perimeter defense against adversaries. It contains *no JavaScript* and uses HTML+CSS with a Python backend. [Project Goals](README_goals.md) - [Commandline interface](README_commandline.md) - [Customizations](README_customizations.md) - [Code of Conduct](code-of-conduct.md) @@ -234,7 +234,7 @@ Please be aware that such installations will not federate with ordinary fedivers ## Custom Fonts -If you want to use a particular font then copy it into the *fonts* directory, rename it as *custom.ttf/woff/woff2/otf* and then restart the epicyon daemon. +If you want to use a particular font then copy it into the *fonts* directory, rename it as *custom.ttf/woff/woff2/otf* and then restart the Epicyon daemon. ``` bash systemctl restart epicyon diff --git a/README_commandline.md b/README_commandline.md index f92a10708..a8b1308b2 100644 --- a/README_commandline.md +++ b/README_commandline.md @@ -1,6 +1,6 @@ -# Commandline Admin +# Command-line Admin -This system can be administrated from the commandline. +This system can be administrated from the command-line. ## Account Management @@ -52,7 +52,7 @@ To remove an account (be careful!): python3 epicyon.py --rmgroup nickname@domain ``` -Setting avatar or changing background is the same as for any other account on the system. You can also moderate a group, applying filters, blocks or a perimeter, in the same way as for other acounts. +Setting avatar or changing background is the same as for any other account on the system. You can also moderate a group, applying filters, blocks or a perimeter, in the same way as for other accounts. ## Defining a perimeter @@ -76,7 +76,7 @@ The password is for the client to obtain access to the server. You may or may not need to use the *--port*, *--https* and *--tor* options, depending upon how your server was set up. -Unfollowing is silimar: +Unfollowing is similar: ``` bash python3 epicyon.py --nickname [yournick] --domain [name] --unfollow othernick@domain --password [c2s password] @@ -131,7 +131,7 @@ To view the public posts for a person: python3 epicyon.py --posts nickname@domain ``` -If you want to view the raw json: +If you want to view the raw JSON: ``` bash python3 epicyon.py --postsraw nickname@domain @@ -156,7 +156,7 @@ xdot socnet.dot ## Delete posts -To delete a post which you wrote you must first know its url. It is usually something like: +To delete a post which you wrote you must first know its URL. It is usually something like: ``` text https://yourDomain/users/yourNickname/statuses/number @@ -177,7 +177,7 @@ Another complication of federated deletion is that the followers collection may ## Announcements/repeats/boosts -To announce or repeat a post you will first need to know it's url. It is usually something like: +To announce or repeat a post you will first need to know it's URL. It is usually something like: ``` text https://domain/users/name/statuses/number @@ -192,7 +192,7 @@ python3 epicyon.py --nickname [yournick] --domain [name] \ ## Like posts -To like a post you will first need to know it's url. It is usually something like: +To like a post you will first need to know it's URL. It is usually something like: ``` text https://domain/users/name/statuses/number @@ -240,7 +240,7 @@ Whether you are using the **--federate** option to define a set of allowed insta python3 epicyon.py --nickname yournick --domain yourdomain --block somenick@somedomain --password [c2s password] ``` -This blocks at the earliest possble stage of receiving messages, such that nothing from the specified account will be written to your inbox. +This blocks at the earliest possible stage of receiving messages, such that nothing from the specified account will be written to your inbox. Or to unblock: @@ -313,7 +313,7 @@ python3 epicyon.py --nickname [admin nickname] --domain [mydomain] \ --password [c2s password] ``` -This extends the ActivityPub client-to-server protocol to include activities called *Delegate* and *Role*. The json looks like: +This extends the ActivityPub client-to-server protocol to include activities called *Delegate* and *Role*. The JSON looks like: ``` json { 'type': 'Delegate', @@ -343,7 +343,7 @@ python3 epicyon.py --nickname [nick] --domain [mydomain] \ The level value is a percentage which indicates how proficient you are with that skill. -This extends the ActivityPub client-to-server protocol to include an activity called *Skill*. The json looks like: +This extends the ActivityPub client-to-server protocol to include an activity called *Skill*. The JSON looks like: ``` json { 'type': 'Skill', @@ -365,7 +365,7 @@ python3 epicyon.py --nickname [nick] --domain [mydomain] \ The status value can be any string, and can become part of organization building by combining it with roles and skills. -This extends the ActivityPub client-to-server protocol to include an activity called *Availability*. "Status" was avoided because of te possibility of confusion with other things. The json looks like: +This extends the ActivityPub client-to-server protocol to include an activity called *Availability*. "Status" was avoided because of the possibility of confusion with other things. The JSON looks like: ``` json { 'type': 'Availability', @@ -377,7 +377,7 @@ This extends the ActivityPub client-to-server protocol to include an activity ca ## Shares -This system includes a feature for bartering or gifting (i.e. common resource pooling or exchange without money), based upon the earlier Sharings plugin made by the Las Indias group which existed within GNU Social. It's intended to operate at the municipal level, sharing physical objects with people in your local vicinity. For example, sharing gardening tools on a street or a 3D printer between makerspaces. +This system includes a feature for bartering or gifting (i.e. common resource pooling or exchange without money), based upon the earlier Sharings plugin made by the Las Indias group which existed within GNU Social. It's intended to operate at the municipal level, sharing physical objects with people in your local vicinity. For example, sharing gardening tools on a street or a 3D printer between maker-spaces. To share an item. @@ -385,7 +385,7 @@ To share an item. python3 epicyon.py --itemName "spanner" --nickname [yournick] --domain [yourdomain] --summary "It's a spanner" --itemType "tool" --itemCategory "mechanical" --location [yourCity] --duration "2 months" --itemImage spanner.png --password [c2s password] ``` -For the duration of the share you can use hours,days,weeks,months or years. +For the duration of the share you can use hours, days, weeks, months, or years. To remove a shared item: diff --git a/README_customizations.md b/README_customizations.md index 841596df8..a6f86ad7d 100644 --- a/README_customizations.md +++ b/README_customizations.md @@ -28,4 +28,4 @@ Extra emoji can be added to the *emoji* directory and you should then update the ## Themes -If you want to create a new theme then the functions for that are within *theme.py*. These functions take the css templates and modify them. You will need to edit *themesDropdown* within *webinterface.py* and add the appropriate translations for the theme name. Themes are selectable from the profile screen of the administrator. +If you want to create a new theme then the functions for that are within *theme.py*. These functions take the CSS templates and modify them. You will need to edit *themesDropdown* within *webinterface.py* and add the appropriate translations for the theme name. Themes are selectable from the profile screen of the administrator. diff --git a/README_goals.md b/README_goals.md index 1fd443003..8403bba5c 100644 --- a/README_goals.md +++ b/README_goals.md @@ -10,22 +10,22 @@ * Attention to accessibility and should be usable in lynx with a screen reader * Remove metadata from attached images, avatars and backgrounds * Support for multiple themes, with ability to create custom themes - * Being able to build crowdsouced organizations with roles and skills + * Being able to build crowd-sourced organizations with roles and skills * Sharings collection, similar to the gnusocial sharings plugin * Quotas for received posts per day, per domain and per account - * Hellthread detection and removal + * Hell-thread detection and removal * Instance and account level federation lists * Support content warnings, reporting and blocking * http signatures and basic auth - * json-LD signatures on outgoing posts, optional on incoming - * Compatible with http (onion addresses, i2p), https and hypercore + * JSON-LD signatures on outgoing posts, optional on incoming + * Compatible with HTTP (onion addresses, i2p), HTTPS and hypercore * Minimal dependencies * Dependencies are maintained Debian packages * Data minimization principle. Configurable post expiry time * Likes and repeats only visible to authorized viewers - * ReplyGuy mitigation - maxmimum replies per post or posts per day + * Reply Guy mitigation - maximum replies per post or posts per day * Ability to delete or hide specific conversation threads - * Commandline interface + * Command-line interface * Simple web interface * Designed for intermittent connectivity. Assume network disruptions * Limited visibility of follows/followers @@ -36,17 +36,17 @@ **Features which won't be implemented** -The following are considered antifeatures of other social network systems, since they encourage dysfunctional social interactions. +The following are considered anti-features of other social network systems, since they encourage dysfunctional social interactions. * Features designed to scale to large numbers of accounts (say, more than 20 active users) * Trending hashtags, or trending anything * Ranking, rating or recommending mechanisms for posts or people (other than likes or repeats/boosts) - * Geolocation features + * Geo-location features * Algorithmic timelines (i.e. non-chronological) * Direct payment mechanisms, although integration with other services may be possible * Any variety of blockchain * Sponsored posts * Enterprise features for use cases applicable only to businesses. Epicyon could be used in a small business, but it's not primarily designed for that - * Collaborative editing of posts, although you could do that outside of this system using etherpad, or similar + * Collaborative editing of posts, although you could do that outside of this system using Etherpad, or similar * Anonymous posts from random internet users published under a single generic instance account * Hierarchies of roles beyond ordinary moderation, such as X requires special agreement from Y before sending a post From dfdc694cfeed72228f407a700142d1b8fa325b16 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 17 Mar 2021 10:04:49 +0000 Subject: [PATCH 02/87] More consistent terminology --- README_commandline.md | 2 +- notifications_client.py => desktop_client.py | 449 ++++++++++--------- epicyon.py | 22 +- 3 files changed, 238 insertions(+), 235 deletions(-) rename notifications_client.py => desktop_client.py (79%) diff --git a/README_commandline.md b/README_commandline.md index 10bdea6e2..62c28f28a 100644 --- a/README_commandline.md +++ b/README_commandline.md @@ -422,7 +422,7 @@ Or if you have picospeaker installed: The desktop client has a few commands, which may be more convenient than the web interface for some purposes: ``` bash -quit Exit from the notification client +quit Exit from the desktop client mute Turn off the screen reader speak Turn on the screen reader sounds on Turn on notification sounds diff --git a/notifications_client.py b/desktop_client.py similarity index 79% rename from notifications_client.py rename to desktop_client.py index 281845967..47571ab2d 100644 --- a/notifications_client.py +++ b/desktop_client.py @@ -1,4 +1,4 @@ -__filename__ = "notifications_client.py" +__filename__ = "desktop_client.py" __author__ = "Bob Mottram" __license__ = "AGPL3+" __version__ = "1.2.0" @@ -45,7 +45,7 @@ def _desktopHelp() -> None: print(indent + 'Commands:') print('') print(indent + 'quit ' + - 'Exit from the notification client') + 'Exit from the desktop client') print(indent + 'show dm|sent|inbox|replies ' + 'Show a timeline') print(indent + 'mute ' + @@ -85,13 +85,13 @@ def _desktopHelp() -> None: print('') -def _clearScreen() -> None: +def _desktopClearScreen() -> None: """Clears the screen """ os.system('cls' if os.name == 'nt' else 'clear') -def _showDesktopBanner() -> None: +def _desktopShowBanner() -> None: """Shows the banner at the top """ bannerFilename = 'banner.txt' @@ -106,9 +106,9 @@ def _showDesktopBanner() -> None: print(banner + '\n') -def _waitForKeypress(timeout: int, debug: bool) -> str: - """Waits for a keypress with a timeout - Returns the key pressed, or None on timeout +def _desktopWaitForCmd(timeout: int, debug: bool) -> str: + """Waits for a command to be entered with a timeout + Returns the command, or None on timeout """ i, o, e = select.select([sys.stdin], [], [], timeout) @@ -224,14 +224,14 @@ def _sayCommand(content: str, sayStr: str, screenreader: str, systemLanguage, espeak) -def _notificationReplyToPost(session, postId: str, - baseDir: str, nickname: str, password: str, - domain: str, port: int, httpPrefix: str, - cachedWebfingers: {}, personCache: {}, - debug: bool, subject: str, - screenreader: str, systemLanguage: str, - espeak) -> None: - """Use the notification client to send a reply to the most recent post +def _desktopReplyToPost(session, postId: str, + baseDir: str, nickname: str, password: str, + domain: str, port: int, httpPrefix: str, + cachedWebfingers: {}, personCache: {}, + debug: bool, subject: str, + screenreader: str, systemLanguage: str, + espeak) -> None: + """Use the desktop client to send a reply to the most recent post """ if '://' not in postId: return @@ -288,14 +288,14 @@ def _notificationReplyToPost(session, postId: str, _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) -def _notificationNewPost(session, - baseDir: str, nickname: str, password: str, - domain: str, port: int, httpPrefix: str, - cachedWebfingers: {}, personCache: {}, - debug: bool, - screenreader: str, systemLanguage: str, - espeak) -> None: - """Use the notification client to create a new post +def _desktopNewPost(session, + baseDir: str, nickname: str, password: str, + domain: str, port: int, httpPrefix: str, + cachedWebfingers: {}, personCache: {}, + debug: bool, + screenreader: str, systemLanguage: str, + espeak) -> None: + """Use the desktop client to create a new post """ sayStr = 'Create new post' _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) @@ -448,11 +448,11 @@ def _readLocalBoxPost(boxName: str, index: int, return speakerJson -def _showLocalBox(notifyJson: {}, boxName: str, - screenreader: str, systemLanguage: str, espeak, - startPostIndex=0, noOfPosts=10, - newReplies=False, - newDMs=False) -> bool: +def _desktopShowBox(notifyJson: {}, boxName: str, + screenreader: str, systemLanguage: str, espeak, + startPostIndex=0, noOfPosts=10, + newReplies=False, + newDMs=False) -> bool: """Shows locally stored posts for a given subdirectory """ indent = ' ' @@ -472,8 +472,8 @@ def _showLocalBox(notifyJson: {}, boxName: str, index.append(f) # title - _clearScreen() - _showDesktopBanner() + _desktopClearScreen() + _desktopShowBanner() notificationIcons = '' if notifyJson: @@ -590,14 +590,14 @@ def _showLocalBox(notifyJson: {}, boxName: str, return True -def _notificationNewDM(session, toHandle: str, - baseDir: str, nickname: str, password: str, - domain: str, port: int, httpPrefix: str, - cachedWebfingers: {}, personCache: {}, - debug: bool, - screenreader: str, systemLanguage: str, - espeak) -> None: - """Use the notification client to create a new direct message +def _desktopNewDM(session, toHandle: str, + baseDir: str, nickname: str, password: str, + domain: str, port: int, httpPrefix: str, + cachedWebfingers: {}, personCache: {}, + debug: bool, + screenreader: str, systemLanguage: str, + espeak) -> None: + """Use the desktop client to create a new direct message which can include multiple destination handles """ if ' ' in toHandle: @@ -611,17 +611,18 @@ def _notificationNewDM(session, toHandle: str, for handle in handlesList: handle = handle.strip() - _notificationNewDMbase(session, handle, - baseDir, nickname, password, - domain, port, httpPrefix, - cachedWebfingers, personCache, - debug, - screenreader, systemLanguage, - espeak) + _desktopNewDMbase(session, handle, + baseDir, nickname, password, + domain, port, httpPrefix, + cachedWebfingers, personCache, + debug, + screenreader, systemLanguage, + espeak) -def _storeMessage(speakerJson: {}, boxName: str) -> None: +def _desktopStoreMsg(speakerJson: {}, boxName: str) -> None: """Stores a message in your home directory for later reading + which could be offline """ if not speakerJson.get('published'): return @@ -646,14 +647,14 @@ def _storeMessage(speakerJson: {}, boxName: str) -> None: saveJson(speakerJson, msgFilename) -def _notificationNewDMbase(session, toHandle: str, - baseDir: str, nickname: str, password: str, - domain: str, port: int, httpPrefix: str, - cachedWebfingers: {}, personCache: {}, - debug: bool, - screenreader: str, systemLanguage: str, - espeak) -> None: - """Use the notification client to create a new direct message +def _desktopNewDMbase(session, toHandle: str, + baseDir: str, nickname: str, password: str, + domain: str, port: int, httpPrefix: str, + cachedWebfingers: {}, personCache: {}, + debug: bool, + screenreader: str, systemLanguage: str, + espeak) -> None: + """Use the desktop client to create a new direct message """ toPort = port if '://' in toHandle: @@ -756,32 +757,32 @@ def _notificationNewDMbase(session, toHandle: str, "id": postId, "direct": True } - _storeMessage(speakerJson, 'sent') + _desktopStoreMsg(speakerJson, 'sent') sayStr = 'Direct message sent' else: sayStr = 'Direct message failed' _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) -def runNotificationsClient(baseDir: str, proxyType: str, httpPrefix: str, - nickname: str, domain: str, port: int, - password: str, screenreader: str, - systemLanguage: str, - notificationSounds: bool, - notificationType: str, - noKeyPress: bool, - storeInboxPosts: bool, - showNewPosts: bool, - debug: bool) -> None: - """Runs the notifications and screen reader client, +def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, + nickname: str, domain: str, port: int, + password: str, screenreader: str, + systemLanguage: str, + notificationSounds: bool, + notificationType: str, + noKeyPress: bool, + storeInboxPosts: bool, + showNewPosts: bool, + debug: bool) -> None: + """Runs the desktop and screen reader client, which announces new inbox items """ indent = ' ' if showNewPosts: indent = '' - _clearScreen() - _showDesktopBanner() + _desktopClearScreen() + _desktopShowBanner() espeak = None if screenreader: @@ -814,12 +815,12 @@ def runNotificationsClient(baseDir: str, proxyType: str, httpPrefix: str, if not showNewPosts: print('') currInboxIndex = 0 - _showLocalBox(None, 'inbox', - screenreader, systemLanguage, espeak, - currInboxIndex, 10) + _desktopShowBox(None, 'inbox', + screenreader, systemLanguage, espeak, + currInboxIndex, 10) currTimeline = 'inbox' print('') - keyPress = _waitForKeypress(2, debug) + commandStr = _desktopWaitForCmd(2, debug) originalScreenReader = screenreader domainFull = getFullDomain(domain, port) @@ -980,20 +981,20 @@ def runNotificationsClient(baseDir: str, proxyType: str, httpPrefix: str, speakerJson['decrypted'] = False if speakerJson.get('replyToYou'): newRepliesExist = True - _storeMessage(speakerJson, 'replies') + _desktopStoreMsg(speakerJson, 'replies') if speakerJson.get('direct'): newDMsExist = True - _storeMessage(speakerJson, 'dm') + _desktopStoreMsg(speakerJson, 'dm') if storeInboxPosts: - _storeMessage(speakerJson, 'inbox') + _desktopStoreMsg(speakerJson, 'inbox') if not showNewPosts: - _clearScreen() - _showLocalBox(notifyJson, currTimeline, - None, systemLanguage, espeak, - currInboxIndex, 10, - newRepliesExist, - newDMsExist) + _desktopClearScreen() + _desktopShowBox(notifyJson, currTimeline, + None, systemLanguage, espeak, + currInboxIndex, 10, + newRepliesExist, + newDMsExist) else: print('') @@ -1005,112 +1006,114 @@ def runNotificationsClient(baseDir: str, proxyType: str, httpPrefix: str, if noKeyPress: time.sleep(10) else: - keyPress = _waitForKeypress(30, debug) - if keyPress: - if keyPress.startswith('/'): - keyPress = keyPress[1:] - if keyPress == 'q' or keyPress == 'quit' or keyPress == 'exit': + commandStr = _desktopWaitForCmd(30, debug) + if commandStr: + if commandStr.startswith('/'): + commandStr = commandStr[1:] + if commandStr == 'q' or \ + commandStr == 'quit' or \ + commandStr == 'exit': sayStr = 'Quit' _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) if screenreader: - keyPress = _waitForKeypress(2, debug) + commandStr = _desktopWaitForCmd(2, debug) break - elif keyPress.startswith('show dm'): + elif commandStr.startswith('show dm'): currDMIndex = 0 - _showLocalBox(notifyJson, 'dm', - screenreader, systemLanguage, espeak, - currDMIndex, 10, - newRepliesExist, newDMsExist) + _desktopShowBox(notifyJson, 'dm', + screenreader, systemLanguage, espeak, + currDMIndex, 10, + newRepliesExist, newDMsExist) currTimeline = 'dm' newDMsExist = False - elif keyPress.startswith('show rep'): + elif commandStr.startswith('show rep'): currRepliesIndex = 0 - _showLocalBox(notifyJson, 'replies', - screenreader, systemLanguage, espeak, - currRepliesIndex, 10, - newRepliesExist, newDMsExist) + _desktopShowBox(notifyJson, 'replies', + screenreader, systemLanguage, espeak, + currRepliesIndex, 10, + newRepliesExist, newDMsExist) currTimeline = 'replies' # Turn off the replies indicator newRepliesExist = False - elif keyPress.startswith('show sen'): + elif commandStr.startswith('show sen'): currSentIndex = 0 - _showLocalBox(notifyJson, 'sent', - screenreader, systemLanguage, espeak, - currSentIndex, 10, - newRepliesExist, newDMsExist) + _desktopShowBox(notifyJson, 'sent', + screenreader, systemLanguage, espeak, + currSentIndex, 10, + newRepliesExist, newDMsExist) currTimeline = 'sent' - elif (keyPress == 'show' or keyPress.startswith('show in') or - keyPress == 'clear'): + elif (commandStr == 'show' or commandStr.startswith('show in') or + commandStr == 'clear'): currInboxIndex = 0 - _showLocalBox(notifyJson, 'inbox', - screenreader, systemLanguage, espeak, - currInboxIndex, 10, - newRepliesExist, newDMsExist) + _desktopShowBox(notifyJson, 'inbox', + screenreader, systemLanguage, espeak, + currInboxIndex, 10, + newRepliesExist, newDMsExist) currTimeline = 'inbox' - elif keyPress.startswith('next'): + elif commandStr.startswith('next'): if currTimeline == 'dm': currDMIndex += 10 - _showLocalBox(notifyJson, 'dm', - screenreader, systemLanguage, espeak, - currDMIndex, 10, - newRepliesExist, newDMsExist) + _desktopShowBox(notifyJson, 'dm', + screenreader, systemLanguage, espeak, + currDMIndex, 10, + newRepliesExist, newDMsExist) elif currTimeline == 'replies': currRepliesIndex += 10 - _showLocalBox(notifyJson, 'replies', - screenreader, systemLanguage, espeak, - currRepliesIndex, 10, - newRepliesExist, newDMsExist) + _desktopShowBox(notifyJson, 'replies', + screenreader, systemLanguage, espeak, + currRepliesIndex, 10, + newRepliesExist, newDMsExist) elif currTimeline == 'sent': currSentIndex += 10 - _showLocalBox(notifyJson, 'sent', - screenreader, systemLanguage, espeak, - currSentIndex, 10, - newRepliesExist, newDMsExist) + _desktopShowBox(notifyJson, 'sent', + screenreader, systemLanguage, espeak, + currSentIndex, 10, + newRepliesExist, newDMsExist) elif currTimeline == 'inbox': currInboxIndex += 10 - _showLocalBox(notifyJson, 'inbox', - screenreader, systemLanguage, espeak, - currInboxIndex, 10, - newRepliesExist, newDMsExist) - elif keyPress.startswith('prev'): + _desktopShowBox(notifyJson, 'inbox', + screenreader, systemLanguage, espeak, + currInboxIndex, 10, + newRepliesExist, newDMsExist) + elif commandStr.startswith('prev'): if currTimeline == 'dm': currDMIndex -= 10 if currDMIndex < 0: currDMIndex = 0 - _showLocalBox(notifyJson, 'dm', - screenreader, systemLanguage, espeak, - currDMIndex, 10, - newRepliesExist, newDMsExist) + _desktopShowBox(notifyJson, 'dm', + screenreader, systemLanguage, espeak, + currDMIndex, 10, + newRepliesExist, newDMsExist) elif currTimeline == 'replies': currRepliesIndex -= 10 if currRepliesIndex < 0: currRepliesIndex = 0 - _showLocalBox(notifyJson, 'replies', - screenreader, systemLanguage, espeak, - currRepliesIndex, 10, - newRepliesExist, newDMsExist) + _desktopShowBox(notifyJson, 'replies', + screenreader, systemLanguage, espeak, + currRepliesIndex, 10, + newRepliesExist, newDMsExist) elif currTimeline == 'sent': currSentIndex -= 10 if currSentIndex < 0: currSentIndex = 0 - _showLocalBox(notifyJson, 'sent', - screenreader, systemLanguage, espeak, - currSentIndex, 10, - newRepliesExist, newDMsExist) + _desktopShowBox(notifyJson, 'sent', + screenreader, systemLanguage, espeak, + currSentIndex, 10, + newRepliesExist, newDMsExist) elif currTimeline == 'inbox': currInboxIndex -= 10 if currInboxIndex < 0: currInboxIndex = 0 - _showLocalBox(notifyJson, 'inbox', - screenreader, systemLanguage, espeak, - currInboxIndex, 10, - newRepliesExist, newDMsExist) - elif keyPress.startswith('read ') or keyPress == 'read': - if keyPress == 'read': + _desktopShowBox(notifyJson, 'inbox', + screenreader, systemLanguage, espeak, + currInboxIndex, 10, + newRepliesExist, newDMsExist) + elif commandStr.startswith('read ') or commandStr == 'read': + if commandStr == 'read': postIndexStr = '1' else: - postIndexStr = keyPress.split('read ')[1] + postIndexStr = commandStr.split('read ')[1] if postIndexStr.isdigit(): postIndex = int(postIndexStr) speakerJson = \ @@ -1118,64 +1121,64 @@ def runNotificationsClient(baseDir: str, proxyType: str, httpPrefix: str, systemLanguage, screenreader, espeak) print('') - elif keyPress == 'reply' or keyPress == 'r': + elif commandStr == 'reply' or commandStr == 'r': if speakerJson.get('id'): postId = speakerJson['id'] subject = None if speakerJson.get('summary'): subject = speakerJson['summary'] sessionReply = createSession(proxyType) - _notificationReplyToPost(sessionReply, postId, - baseDir, nickname, password, - domain, port, httpPrefix, - cachedWebfingers, personCache, - debug, subject, - screenreader, systemLanguage, - espeak) + _desktopReplyToPost(sessionReply, postId, + baseDir, nickname, password, + domain, port, httpPrefix, + cachedWebfingers, personCache, + debug, subject, + screenreader, systemLanguage, + espeak) print('') - elif (keyPress == 'post' or keyPress == 'p' or - keyPress == 'send' or - keyPress.startswith('dm ') or - keyPress.startswith('direct message ') or - keyPress.startswith('post ') or - keyPress.startswith('send ')): + elif (commandStr == 'post' or commandStr == 'p' or + commandStr == 'send' or + commandStr.startswith('dm ') or + commandStr.startswith('direct message ') or + commandStr.startswith('post ') or + commandStr.startswith('send ')): sessionPost = createSession(proxyType) - if keyPress.startswith('dm ') or \ - keyPress.startswith('direct message ') or \ - keyPress.startswith('post ') or \ - keyPress.startswith('send '): - keyPress = keyPress.replace(' to ', ' ') - keyPress = keyPress.replace(' dm ', ' ') - keyPress = keyPress.replace(' DM ', ' ') + if commandStr.startswith('dm ') or \ + commandStr.startswith('direct message ') or \ + commandStr.startswith('post ') or \ + commandStr.startswith('send '): + commandStr = commandStr.replace(' to ', ' ') + commandStr = commandStr.replace(' dm ', ' ') + commandStr = commandStr.replace(' DM ', ' ') # direct message toHandle = None - if keyPress.startswith('post '): - toHandle = keyPress.split('post ', 1)[1] - elif keyPress.startswith('send '): - toHandle = keyPress.split('send ', 1)[1] - elif keyPress.startswith('dm '): - toHandle = keyPress.split('dm ', 1)[1] - elif keyPress.startswith('direct message '): - toHandle = keyPress.split('direct message ', 1)[1] + if commandStr.startswith('post '): + toHandle = commandStr.split('post ', 1)[1] + elif commandStr.startswith('send '): + toHandle = commandStr.split('send ', 1)[1] + elif commandStr.startswith('dm '): + toHandle = commandStr.split('dm ', 1)[1] + elif commandStr.startswith('direct message '): + toHandle = commandStr.split('direct message ', 1)[1] if toHandle: - _notificationNewDM(sessionPost, toHandle, - baseDir, nickname, password, - domain, port, httpPrefix, - cachedWebfingers, personCache, - debug, - screenreader, systemLanguage, - espeak) + _desktopNewDM(sessionPost, toHandle, + baseDir, nickname, password, + domain, port, httpPrefix, + cachedWebfingers, personCache, + debug, + screenreader, systemLanguage, + espeak) else: # public post - _notificationNewPost(sessionPost, - baseDir, nickname, password, - domain, port, httpPrefix, - cachedWebfingers, personCache, - debug, - screenreader, systemLanguage, - espeak) + _desktopNewPost(sessionPost, + baseDir, nickname, password, + domain, port, httpPrefix, + cachedWebfingers, personCache, + debug, + screenreader, systemLanguage, + espeak) print('') - elif keyPress == 'like': + elif commandStr == 'like': if speakerJson.get('id'): sayStr = 'Liking post by ' + speakerJson['name'] _sayCommand(sayStr, sayStr, @@ -1189,7 +1192,7 @@ def runNotificationsClient(baseDir: str, proxyType: str, httpPrefix: str, cachedWebfingers, personCache, False, __version__) print('') - elif keyPress == 'unlike' or keyPress == 'undo like': + elif commandStr == 'unlike' or commandStr == 'undo like': if speakerJson.get('id'): sayStr = 'Undoing like of post by ' + speakerJson['name'] _sayCommand(sayStr, sayStr, @@ -1203,9 +1206,9 @@ def runNotificationsClient(baseDir: str, proxyType: str, httpPrefix: str, cachedWebfingers, personCache, False, __version__) print('') - elif (keyPress == 'announce' or - keyPress == 'boost' or - keyPress == 'retweet'): + elif (commandStr == 'announce' or + commandStr == 'boost' or + commandStr == 'retweet'): if speakerJson.get('id'): postId = speakerJson['id'] sayStr = 'Announcing post by ' + speakerJson['name'] @@ -1220,8 +1223,8 @@ def runNotificationsClient(baseDir: str, proxyType: str, httpPrefix: str, cachedWebfingers, personCache, True, __version__) print('') - elif keyPress.startswith('follow '): - followHandle = keyPress.replace('follow ', '').strip() + elif commandStr.startswith('follow '): + followHandle = commandStr.replace('follow ', '').strip() if followHandle.startswith('@'): followHandle = followHandle[1:] if '@' in followHandle or '://' in followHandle: @@ -1249,9 +1252,9 @@ def runNotificationsClient(baseDir: str, proxyType: str, httpPrefix: str, _sayCommand(sayStr, screenreader, systemLanguage, espeak) print('') - elif (keyPress.startswith('unfollow ') or - keyPress.startswith('stop following ')): - followHandle = keyPress.replace('unfollow ', '').strip() + elif (commandStr.startswith('unfollow ') or + commandStr.startswith('stop following ')): + followHandle = commandStr.replace('unfollow ', '').strip() followHandle = followHandle.replace('stop following ', '') if followHandle.startswith('@'): followHandle = followHandle[1:] @@ -1280,9 +1283,9 @@ def runNotificationsClient(baseDir: str, proxyType: str, httpPrefix: str, _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) print('') - elif (keyPress == 'repeat' or keyPress == 'replay' or - keyPress == 'rp' or keyPress == 'again' or - keyPress == 'say again'): + elif (commandStr == 'repeat' or commandStr == 'replay' or + commandStr == 'rp' or commandStr == 'again' or + commandStr == 'say again'): if screenreader and nameStr and \ gender and messageStr and content: sayStr = 'Repeating ' + nameStr @@ -1294,25 +1297,25 @@ def runNotificationsClient(baseDir: str, proxyType: str, httpPrefix: str, systemLanguage, espeak, nameStr, gender) print('') - elif (keyPress == 'sounds on' or - keyPress == 'sound on' or - keyPress == 'sound'): + elif (commandStr == 'sounds on' or + commandStr == 'sound on' or + commandStr == 'sound'): sayStr = 'Notification sounds on' _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) notificationSounds = True - elif (keyPress == 'sounds off' or - keyPress == 'sound off' or - keyPress == 'nosound'): + elif (commandStr == 'sounds off' or + commandStr == 'sound off' or + commandStr == 'nosound'): sayStr = 'Notification sounds off' _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) notificationSounds = False - elif (keyPress == 'speak' or - keyPress == 'screen reader on' or - keyPress == 'speaker on' or - keyPress == 'talker on' or - keyPress == 'reader on'): + elif (commandStr == 'speak' or + commandStr == 'screen reader on' or + commandStr == 'speaker on' or + commandStr == 'talker on' or + commandStr == 'reader on'): if originalScreenReader: screenreader = originalScreenReader sayStr = 'Screen reader on' @@ -1320,11 +1323,11 @@ def runNotificationsClient(baseDir: str, proxyType: str, httpPrefix: str, systemLanguage, espeak) else: print('No --screenreader option was specified') - elif (keyPress == 'mute' or - keyPress == 'screen reader off' or - keyPress == 'speaker off' or - keyPress == 'talker off' or - keyPress == 'reader off'): + elif (commandStr == 'mute' or + commandStr == 'screen reader off' or + commandStr == 'speaker off' or + commandStr == 'talker off' or + commandStr == 'reader off'): if originalScreenReader: screenreader = None sayStr = 'Screen reader off' @@ -1332,10 +1335,10 @@ def runNotificationsClient(baseDir: str, proxyType: str, httpPrefix: str, systemLanguage, espeak) else: print('No --screenreader option was specified') - elif keyPress.startswith('open'): + elif commandStr.startswith('open'): currIndex = 0 - if ' ' in keyPress: - postIndex = keyPress.split(' ')[-1].strip() + if ' ' in commandStr: + postIndex = commandStr.split(' ')[-1].strip() if postIndex.isdigit(): currIndex = int(postIndex) speakerJson = \ @@ -1358,7 +1361,7 @@ def runNotificationsClient(baseDir: str, proxyType: str, httpPrefix: str, _sayCommand(sayStr, sayStr, originalScreenReader, systemLanguage, espeak) print('') - elif keyPress.startswith('accept'): + elif commandStr.startswith('accept'): if notifyJson: if notifyJson.get('followRequestsList'): if len(notifyJson['followRequestsList']) > 0: @@ -1371,7 +1374,7 @@ def runNotificationsClient(baseDir: str, proxyType: str, httpPrefix: str, _sayCommand(sayStr, sayStr, originalScreenReader, systemLanguage, espeak) print('') - elif keyPress.startswith('reject'): + elif commandStr.startswith('reject'): if notifyJson: if notifyJson.get('followRequestsList'): if len(notifyJson['followRequestsList']) > 0: @@ -1384,5 +1387,5 @@ def runNotificationsClient(baseDir: str, proxyType: str, httpPrefix: str, _sayCommand(sayStr, sayStr, originalScreenReader, systemLanguage, espeak) print('') - elif keyPress.startswith('h'): + elif commandStr.startswith('h'): _desktopHelp() diff --git a/epicyon.py b/epicyon.py index 567c5372b..b43388621 100644 --- a/epicyon.py +++ b/epicyon.py @@ -78,7 +78,7 @@ from theme import setTheme from announce import sendAnnounceViaServer from socnet import instancesGraph from migrate import migrateAccounts -from notifications_client import runNotificationsClient +from desktop_client import runDesktopClient def str2bool(v) -> bool: @@ -304,7 +304,7 @@ parser.add_argument("--notifyShowNewPosts", dest='notifyShowNewPosts', type=str2bool, nargs='?', const=True, default=False, - help="Notification client shows/speaks new posts " + + help="Desktop client shows/speaks new posts " + "as they arrive") parser.add_argument("--noapproval", type=str2bool, nargs='?', const=True, default=False, @@ -1868,15 +1868,15 @@ if args.desktop: # only store inbox posts if we are not running as a daemon storeInboxPosts = not args.noKeyPress - runNotificationsClient(baseDir, proxyType, httpPrefix, - nickname, domain, port, args.password, - args.screenreader, args.language, - args.notificationSounds, - args.notificationType, - args.noKeyPress, - storeInboxPosts, - args.notifyShowNewPosts, - args.debug) + runDesktopClient(baseDir, proxyType, httpPrefix, + nickname, domain, port, args.password, + args.screenreader, args.language, + args.notificationSounds, + args.notificationType, + args.noKeyPress, + storeInboxPosts, + args.notifyShowNewPosts, + args.debug) sys.exit() if federationList: From 3a83c46650847809472cb9ea822b84318831c1ee Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 17 Mar 2021 10:33:21 +0000 Subject: [PATCH 03/87] Wait after reading a post --- desktop_client.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/desktop_client.py b/desktop_client.py index 47571ab2d..32c8c5211 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -821,6 +821,7 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, currTimeline = 'inbox' print('') commandStr = _desktopWaitForCmd(2, debug) + nextCommandStr = None originalScreenReader = screenreader domainFull = getFullDomain(domain, port) @@ -1006,7 +1007,11 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, if noKeyPress: time.sleep(10) else: - commandStr = _desktopWaitForCmd(30, debug) + if nextCommandStr: + commandStr = nextCommandStr + nextCommandStr = None + else: + commandStr = _desktopWaitForCmd(30, debug) if commandStr: if commandStr.startswith('/'): commandStr = commandStr[1:] @@ -1120,6 +1125,19 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, _readLocalBoxPost(currTimeline, postIndex, systemLanguage, screenreader, espeak) + # if we are on a busy timeline then wait for the post + # to be read because otherwise it could potentially be + # immediately overwritten as the timeline refreshes + if speakerJson and not noKeyPress: + # average reading speed is said to be 800 chars/min + # so this allows some overhead + readingSpeedCharsPerMin = 600 + displayTimeSec = \ + int(len(speakerJson['say']) * 60 / + readingSpeedCharsPerMin) + print('Waiting ' + str(displayTimeSec) + ' sec.') + nextCommandStr = \ + _desktopWaitForCmd(displayTimeSec, debug) print('') elif commandStr == 'reply' or commandStr == 'r': if speakerJson.get('id'): From 806209b3b58170ba4b1214ee15aa43412023c1fd Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 17 Mar 2021 10:35:40 +0000 Subject: [PATCH 04/87] Minimum of 10 second reading time --- desktop_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/desktop_client.py b/desktop_client.py index 32c8c5211..9958ff934 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -1135,7 +1135,8 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, displayTimeSec = \ int(len(speakerJson['say']) * 60 / readingSpeedCharsPerMin) - print('Waiting ' + str(displayTimeSec) + ' sec.') + if displayTimeSec < 10: + displayTimeSec = 10 nextCommandStr = \ _desktopWaitForCmd(displayTimeSec, debug) print('') From 67f63c511902b989cc136222749c077c300fe216 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 17 Mar 2021 20:18:00 +0000 Subject: [PATCH 05/87] Set pgp public key from desktop client --- desktop_client.py | 14 ++++ epicyon.py | 20 +++-- follow.py | 2 +- outbox.py | 122 +++++++++++++++++++++++++++ person.py | 33 +++++--- pgp.py | 205 ++++++++++++++++++++++++++++++++++++++++++++++ tests.py | 119 ++++++++++++++++++++++++++- 7 files changed, 492 insertions(+), 23 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index 9958ff934..cf2ac8e5c 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -35,6 +35,7 @@ from announce import sendAnnounceViaServer from pgp import pgpDecrypt from pgp import hasLocalPGPkey from pgp import pgpEncryptToActor +from pgp import pgpPublicKeyUpload def _desktopHelp() -> None: @@ -850,8 +851,21 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, newRepliesExist = False newDMsExist = False currPostId = '' + pgpKeyUpload = False while (1): session = createSession(proxyType) + + if not pgpKeyUpload: + pgpKey = \ + pgpPublicKeyUpload(baseDir, session, + nickname, password, + domain, port, httpPrefix, + cachedWebfingers, personCache, + debug, False) + if pgpKey: + print('PGP public key uploaded') + pgpKeyUpload = True + notifyJson = None speakerJson = \ getSpeakerFromServer(baseDir, session, nickname, password, diff --git a/epicyon.py b/epicyon.py index b43388621..0b51d50f5 100644 --- a/epicyon.py +++ b/epicyon.py @@ -48,6 +48,7 @@ from follow import sendUnfollowRequestViaServer from tests import testPostMessageBetweenServers from tests import testFollowBetweenServers from tests import testClientToServer +from tests import testUpdateActor from tests import runAllTests from auth import storeBasicCredentials from auth import createPassword @@ -531,6 +532,7 @@ if args.testsnetwork: testPostMessageBetweenServers() testFollowBetweenServers() testClientToServer() + testUpdateActor() print('All tests succeeded') sys.exit() @@ -2121,7 +2123,7 @@ if args.testdata: testFollowersOnly = False testSaveToFile = True - testClientToServer = False + testC2S = False testCommentsEnabled = True testAttachImageFilename = None testMediaType = None @@ -2131,7 +2133,7 @@ if args.testdata: "like this is totally just a #test man", testFollowersOnly, testSaveToFile, - testClientToServer, + testC2S, testCommentsEnabled, testAttachImageFilename, testMediaType, testImageDescription) @@ -2139,7 +2141,7 @@ if args.testdata: "Zoiks!!!", testFollowersOnly, testSaveToFile, - testClientToServer, + testC2S, testCommentsEnabled, testAttachImageFilename, testMediaType, testImageDescription) @@ -2147,7 +2149,7 @@ if args.testdata: "Hey scoob we need like a hundred more #milkshakes", testFollowersOnly, testSaveToFile, - testClientToServer, + testC2S, testCommentsEnabled, testAttachImageFilename, testMediaType, testImageDescription) @@ -2155,7 +2157,7 @@ if args.testdata: "Getting kinda spooky around here", testFollowersOnly, testSaveToFile, - testClientToServer, + testC2S, testCommentsEnabled, testAttachImageFilename, testMediaType, testImageDescription, @@ -2165,7 +2167,7 @@ if args.testdata: "if it wasn't for those pesky hackers", testFollowersOnly, testSaveToFile, - testClientToServer, + testC2S, testCommentsEnabled, 'img/logo.png', 'image/png', 'Description of image') @@ -2173,7 +2175,7 @@ if args.testdata: "man these centralized sites are like the worst!", testFollowersOnly, testSaveToFile, - testClientToServer, + testC2S, testCommentsEnabled, testAttachImageFilename, testMediaType, testImageDescription) @@ -2181,7 +2183,7 @@ if args.testdata: "another mystery solved #test", testFollowersOnly, testSaveToFile, - testClientToServer, + testC2S, testCommentsEnabled, testAttachImageFilename, testMediaType, testImageDescription) @@ -2189,7 +2191,7 @@ if args.testdata: "let's go bowling", testFollowersOnly, testSaveToFile, - testClientToServer, + testC2S, testCommentsEnabled, testAttachImageFilename, testMediaType, testImageDescription) diff --git a/follow.py b/follow.py index 6404eda2a..6da40f642 100644 --- a/follow.py +++ b/follow.py @@ -1028,7 +1028,7 @@ def sendFollowRequestViaServer(baseDir: str, session, postJson(session, newFollowJson, [], inboxUrl, headers, 30, True) if not postResult: if debug: - print('DEBUG: POST announce failed for c2s to ' + inboxUrl) + print('DEBUG: POST follow failed for c2s to ' + inboxUrl) return 5 if debug: diff --git a/outbox.py b/outbox.py index 7726cf6fb..038e069eb 100644 --- a/outbox.py +++ b/outbox.py @@ -21,6 +21,8 @@ from utils import removeIdEnding from utils import getDomainFromActor from utils import dangerousMarkup from utils import isFeaturedWriter +from utils import loadJson +from utils import saveJson from blocking import isBlockedDomain from blocking import outboxBlock from blocking import outboxUndoBlock @@ -42,6 +44,119 @@ from shares import outboxShareUpload from shares import outboxUndoShareUpload +def _outboxPersonReceiveUpdate(recentPostsCache: {}, + baseDir: str, httpPrefix: str, + nickname: str, domain: str, port: int, + messageJson: {}, debug: bool) -> None: + """ Receive an actor update from c2s + For example, setting the PGP key from the desktop client + """ + # these attachments are updatable via c2s + updatableAttachments = ('PGP', 'OpenPGP', 'Email') + + if not messageJson.get('type'): + return + print("messageJson['type'] " + messageJson['type']) + if messageJson['type'] != 'Update': + return + if not messageJson.get('object'): + return + if not isinstance(messageJson['object'], dict): + if debug: + print('DEBUG: c2s actor update object is not dict') + return + if not messageJson['object'].get('type'): + if debug: + print('DEBUG: c2s actor update - no type') + return + if messageJson['object']['type'] != 'Person': + if debug: + print('DEBUG: not a c2s actor update') + return + if not messageJson.get('to'): + if debug: + print('DEBUG: c2s actor update has no "to" field') + return + if not messageJson.get('actor'): + if debug: + print('DEBUG: c2s actor update has no actor field') + return + if not messageJson.get('id'): + if debug: + print('DEBUG: c2s actor update has no id field') + return + actor = \ + httpPrefix + '://' + getFullDomain(domain, port) + '/users/' + nickname + if len(messageJson['to']) != 1: + if debug: + print('DEBUG: c2s actor update - to does not contain one actor ' + + messageJson['to']) + return + if messageJson['to'][0] != actor: + if debug: + print('DEBUG: c2s actor update - to does not contain actor ' + + messageJson['to'] + ' ' + actor) + return + if not messageJson['id'].startswith(actor + '#updates/'): + if debug: + print('DEBUG: c2s actor update - unexpected id ' + + messageJson['id']) + return + updatedActorJson = messageJson['object'] + # load actor from file + actorFilename = baseDir + '/accounts/' + nickname + '@' + domain + '.json' + if not os.path.isfile(actorFilename): + print('actorFilename not found: ' + actorFilename) + return + actorJson = loadJson(actorFilename) + if not actorJson: + return + actorChanged = False + # update fields within actor + if 'attachment' in updatedActorJson: + for newPropertyValue in updatedActorJson['attachment']: + if not newPropertyValue.get('name'): + continue + if newPropertyValue['name'] not in updatableAttachments: + continue + if not newPropertyValue.get('type'): + continue + if not newPropertyValue.get('value'): + continue + if newPropertyValue['type'] != 'PropertyValue': + continue + if 'attachment' in actorJson: + found = False + for propertyValue in actorJson['attachment']: + if propertyValue != 'PropertyValue': + continue + if propertyValue['name'] == newPropertyValue['name']: + if propertyValue['value'] != \ + newPropertyValue['value']: + propertyValue['value'] = \ + newPropertyValue['value'] + actorChanged = True + found = True + break + if not found: + actorJson['attachment'].append({ + "name": newPropertyValue['name'], + "type": "PropertyValue", + "value": newPropertyValue['value'] + }) + actorChanged = True + # save actor to file + if actorChanged: + saveJson(actorJson, actorFilename) + if debug: + print('actor saved: ' + actorFilename) + if debug: + print('New attachment: ' + str(actorJson['attachment'])) + messageJson['object'] = actorJson + if debug: + print('DEBUG: actor update via c2s - ' + nickname + '@' + domain) + + def postMessageToOutbox(session, translate: {}, messageJson: {}, postToNickname: str, server, baseDir: str, httpPrefix: str, @@ -408,6 +523,13 @@ def postMessageToOutbox(session, translate: {}, postToNickname, domain, port, messageJson, debug) + if debug: + print('DEBUG: handle actor updates from c2s') + _outboxPersonReceiveUpdate(recentPostsCache, + baseDir, httpPrefix, + postToNickname, domain, port, + messageJson, debug) + if debug: print('DEBUG: sending c2s post to named addresses') if messageJson.get('to'): diff --git a/person.py b/person.py index 2d6be5a4d..a9b855423 100644 --- a/person.py +++ b/person.py @@ -1106,6 +1106,8 @@ def getActorJson(handle: str, http: bool, gnunet: bool, debug: bool, quiet=False) -> {}: """Returns the actor json """ + if debug: + print('getActorJson for ' + handle) originalActor = handle if '/@' in handle or \ '/users/' in handle or \ @@ -1117,8 +1119,8 @@ def getActorJson(handle: str, http: bool, gnunet: bool, handle = handle.replace(prefix, '') handle = handle.replace('/@', '/users/') if not hasUsersPath(handle): - if not quiet: - print('Expected actor format: ' + + if not quiet or debug: + print('getActorJson: Expected actor format: ' + 'https://domain/@nick or https://domain/users/nick') return None if '/users/' in handle: @@ -1145,13 +1147,13 @@ def getActorJson(handle: str, http: bool, gnunet: bool, # format: @nick@domain if '@' not in handle: if not quiet: - print('Syntax: --actor nickname@domain') + print('getActorJson Syntax: --actor nickname@domain') return None if handle.startswith('@'): handle = handle[1:] if '@' not in handle: if not quiet: - print('Syntax: --actor nickname@domain') + print('getActorJsonSyntax: --actor nickname@domain') return None nickname = handle.split('@')[0] domain = handle.split('@')[1] @@ -1168,7 +1170,10 @@ def getActorJson(handle: str, http: bool, gnunet: bool, httpPrefix = 'gnunet' proxyType = 'gnunet' else: - httpPrefix = 'https' + if '127.0.' not in domain and '192.168.' not in domain: + httpPrefix = 'https' + else: + httpPrefix = 'http' session = createSession(proxyType) if nickname == 'inbox': nickname = domain @@ -1179,12 +1184,12 @@ def getActorJson(handle: str, http: bool, gnunet: bool, None, __version__, debug) if not wfRequest: if not quiet: - print('Unable to webfinger ' + handle) + print('getActorJson Unable to webfinger ' + handle) return None if not isinstance(wfRequest, dict): if not quiet: - print('Webfinger for ' + handle + ' did not return a dict. ' + - str(wfRequest)) + print('getActorJson Webfinger for ' + handle + + ' did not return a dict. ' + str(wfRequest)) return None if not quiet: @@ -1192,11 +1197,13 @@ def getActorJson(handle: str, http: bool, gnunet: bool, personUrl = None if wfRequest.get('errors'): - if not quiet: - print('wfRequest error: ' + str(wfRequest['errors'])) + if not quiet or debug: + print('getActorJson wfRequest error: ' + str(wfRequest['errors'])) if hasUsersPath(handle): personUrl = originalActor else: + if debug: + print('No users path in ' + handle) return None profileStr = 'https://www.w3.org/ns/activitystreams' @@ -1228,8 +1235,9 @@ def getActorJson(handle: str, http: bool, gnunet: bool, getJson(session, personUrl, asHeader, None, debug, __version__, httpPrefix, None, 20, quiet) if personJson: - if not quiet: + if not quiet or debug: pprint(personJson) + return personJson else: profileStr = 'https://www.w3.org/ns/activitystreams' asHeader = { @@ -1238,8 +1246,9 @@ def getActorJson(handle: str, http: bool, gnunet: bool, personJson = \ getJson(session, personUrl, asHeader, None, debug, __version__, httpPrefix, None) - if not quiet: + if not quiet or debug: if personJson: + print('getActorJson returned actor') pprint(personJson) else: print('Failed to get ' + personUrl) diff --git a/pgp.py b/pgp.py index 5724127a4..f369727c2 100644 --- a/pgp.py +++ b/pgp.py @@ -7,11 +7,18 @@ __email__ = "bob@freedombone.net" __status__ = "Production" import os +import time import subprocess from pathlib import Path from person import getActorJson from utils import containsPGPPublicKey from utils import isPGPEncrypted +from utils import getFullDomain +from utils import getStatusNumber +from webfinger import webfingerHandle +from posts import getPersonBox +from auth import createBasicAuthHeader +from session import postJson def getEmailAddress(actorJson: {}) -> str: @@ -395,3 +402,201 @@ def pgpDecrypt(content: str, fromHandle: str) -> str: return content decryptResult = decryptResult.decode('utf-8').strip() return decryptResult + + +def _pgpLocalPublicKeyId() -> str: + """Gets the local pgp public key ID + """ + cmdStr = \ + "gpgconf --list-options gpg | " + \ + "awk -F: '$1 == \"default-key\" {print $10}'" + proc = subprocess.Popen([cmdStr], + stdout=subprocess.PIPE, shell=True) + (result, err) = proc.communicate() + if err: + return None + if not result: + return None + if len(result) < 5: + return None + return result.replace('"', '').strip() + + +def _pgpLocalPublicKey() -> str: + """Gets the local pgp public key + """ + keyId = _pgpLocalPublicKey() + if not keyId: + return None + cmdStr = "gpg --armor --export " + keyId + proc = subprocess.Popen([cmdStr], + stdout=subprocess.PIPE, shell=True) + (result, err) = proc.communicate() + if err: + return None + if not result: + return None + return extractPGPPublicKey(result) + + +def pgpPublicKeyUpload(baseDir: str, session, + nickname: str, password: str, + domain: str, port: int, + httpPrefix: str, + cachedWebfingers: {}, personCache: {}, + debug: bool, test: str) -> {}: + if debug: + print('pgpPublicKeyUpload') + + if not session: + if debug: + print('WARN: No session for pgpPublicKeyUpload') + return None + + if not test: + if debug: + print('Getting PGP public key') + PGPpubKey = _pgpLocalPublicKey() + if not PGPpubKey: + return None + PGPpubKeyId = _pgpLocalPublicKeyId() + else: + if debug: + print('Testing with PGP public key ' + test) + PGPpubKey = test + PGPpubKeyId = None + + domainFull = getFullDomain(domain, port) + if debug: + print('PGP test domain: ' + domainFull) + + handle = nickname + '@' + domainFull + + if debug: + print('Getting actor for ' + handle) + + actorJson = getActorJson(handle, False, False, True) + if not actorJson: + if debug: + print('No actor returned for ' + handle) + return None + + if debug: + print('Actor for ' + handle + ' obtained') + + actor = httpPrefix + '://' + domainFull + '/users/' + nickname + handle = actor.replace('/users/', '/@') + + # check that this looks like the correct actor + if not actorJson.get('id'): + if debug: + print('Actor has no id') + return None + if not actorJson.get('url'): + if debug: + print('Actor has no url') + return None + if not actorJson.get('type'): + if debug: + print('Actor has no type') + return None + if actorJson['id'] != actor: + if debug: + print('Actor id is not ' + actor + + ' instead is ' + actorJson['id']) + return None + if actorJson['url'] != handle: + if debug: + print('Actor url is not ' + handle) + return None + if actorJson['type'] != 'Person': + if debug: + print('Actor type is not Person') + return None + + # set the pgp details + if PGPpubKeyId: + setPGPfingerprint(actorJson, PGPpubKeyId) + else: + if debug: + print('No PGP key Id. Continuing anyway.') + + if debug: + print('Setting PGP key within ' + actor) + setPGPpubKey(actorJson, PGPpubKey) + + # create an actor update + statusNumber, published = getStatusNumber() + actorUpdate = { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': actor + '#updates/' + statusNumber, + 'type': 'Update', + 'actor': actor, + 'to': [actor], + 'cc': [], + 'object': actorJson + } + if debug: + print('actor update is ' + str(actorUpdate)) + + # lookup the inbox for the To handle + wfRequest = \ + webfingerHandle(session, handle, httpPrefix, cachedWebfingers, + domain, __version__, debug) + if not wfRequest: + if debug: + print('DEBUG: pgp actor update webfinger failed for ' + + handle) + return None + if not isinstance(wfRequest, dict): + if debug: + print('WARN: Webfinger for ' + handle + + ' did not return a dict. ' + str(wfRequest)) + return None + + postToBox = 'outbox' + + # get the actor inbox for the To handle + (inboxUrl, pubKeyId, pubKey, + fromPersonId, sharedInbox, avatarUrl, + displayName) = getPersonBox(baseDir, session, wfRequest, personCache, + __version__, httpPrefix, nickname, + domain, postToBox, 52025) + + if not inboxUrl: + if debug: + print('DEBUG: No ' + postToBox + ' was found for ' + handle) + return None + if not fromPersonId: + if debug: + print('DEBUG: No actor was found for ' + handle) + return None + + authHeader = createBasicAuthHeader(nickname, password) + + headers = { + 'host': domain, + 'Content-type': 'application/json', + 'Authorization': authHeader + } + tries = 0 + quiet = not debug + while tries < 4: + postResult = \ + postJson(session, actorUpdate, [], inboxUrl, + headers, 30, quiet) + if postResult: + break + time.sleep(2) + tries += 1 + + if postResult is None: + if debug: + print('DEBUG: POST pgp actor update failed for c2s to ' + + inboxUrl) + return None + + if debug: + print('DEBUG: c2s POST pgp actor update success') + + return actorUpdate diff --git a/tests.py b/tests.py index a3ba76b63..6a232dc71 100644 --- a/tests.py +++ b/tests.py @@ -53,6 +53,7 @@ from utils import getFollowersOfPerson from utils import removeHtml from utils import dangerousMarkup from pgp import extractPGPPublicKey +from pgp import pgpPublicKeyUpload from utils import containsPGPPublicKey from follow import followerOfPerson from follow import unfollowAccount @@ -1574,7 +1575,7 @@ def testClientToServer(): sessionAlice = createSession(proxyType) followersOnly = False - attachedImageFilename = baseDir+'/img/logo.png' + attachedImageFilename = baseDir + '/img/logo.png' mediaType = getAttachmentMediaType(attachedImageFilename) attachedImageDescription = 'Logo' isArticle = False @@ -3447,6 +3448,122 @@ def testExtractPGPPublicKey(): assert result == pubKey +def testUpdateActor(): + print('Testing update of actor properties') + + global testServerAliceRunning + testServerAliceRunning = False + + httpPrefix = 'http' + proxyType = None + federationList = [] + + baseDir = os.getcwd() + if os.path.isdir(baseDir + '/.tests'): + shutil.rmtree(baseDir + '/.tests') + os.mkdir(baseDir + '/.tests') + + # create the server + aliceDir = baseDir + '/.tests/alice' + aliceDomain = '127.0.0.11' + alicePort = 61792 + aliceSendThreads = [] + bobAddress = '127.0.0.84:6384' + + global thrAlice + if thrAlice: + while thrAlice.is_alive(): + thrAlice.stop() + time.sleep(1) + thrAlice.kill() + + thrAlice = \ + threadWithTrace(target=createServerAlice, + args=(aliceDir, aliceDomain, alicePort, bobAddress, + federationList, False, False, + aliceSendThreads), + daemon=True) + + thrAlice.start() + assert thrAlice.is_alive() is True + + # wait for server to be running + ctr = 0 + while not testServerAliceRunning: + time.sleep(1) + ctr += 1 + if ctr > 60: + break + print('Alice online: ' + str(testServerAliceRunning)) + + print('\n\n*******************************************************') + print('Alice updates her PGP key') + + sessionAlice = createSession(proxyType) + cachedWebfingers = {} + personCache = {} + password = 'alicepass' + outboxPath = aliceDir + '/accounts/alice@' + aliceDomain + '/outbox' + actorFilename = aliceDir + '/accounts/' + 'alice@' + aliceDomain + '.json' + assert os.path.isfile(actorFilename) + assert len([name for name in os.listdir(outboxPath) + if os.path.isfile(os.path.join(outboxPath, name))]) == 0 + pubKey = \ + '-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n' + \ + 'mDMEWZBueBYJKwYBBAHaRw8BAQdAKx1t6wL0RTuU6/' + \ + 'IBjngMbVJJ3Wg/3UW73/PV\n' + \ + 'I47xKTS0IUJvYiBNb3R0cmFtIDxib2JAZnJlZWRvb' + \ + 'WJvbmUubmV0PoiQBBMWCAA4\n' + \ + 'FiEEmruCwAq/OfgmgEh9zCU2GR+nwz8FAlmQbngCG' + \ + 'wMFCwkIBwMFFQoJCAsFFgID\n' + \ + 'AQACHgECF4AACgkQzCU2GR+nwz/9sAD/YgsHnVszH' + \ + 'Nz1zlVc5EgY1ByDupiJpHj0\n' + \ + 'XsLYk3AbNRgBALn45RqgD4eWHpmOriH09H5Rc5V9i' + \ + 'N4+OiGUn2AzJ6oHuDgEWZBu\n' + \ + 'eBIKKwYBBAGXVQEFAQEHQPRBG2ZQJce475S3e0Dxe' + \ + 'b0Fz5WdEu2q3GYLo4QG+4Ry\n' + \ + 'AwEIB4h4BBgWCAAgFiEEmruCwAq/OfgmgEh9zCU2G' + \ + 'R+nwz8FAlmQbngCGwwACgkQ\n' + \ + 'zCU2GR+nwz+OswD+JOoyBku9FzuWoVoOevU2HH+bP' + \ + 'OMDgY2OLnST9ZSyHkMBAMcK\n' + \ + 'fnaZ2Wi050483Sj2RmQRpb99Dod7rVZTDtCqXk0J\n' + \ + '=gv5G\n' + \ + '-----END PGP PUBLIC KEY BLOCK-----' + actorUpdate = \ + pgpPublicKeyUpload(aliceDir, sessionAlice, + 'alice', password, + aliceDomain, alicePort, + httpPrefix, + cachedWebfingers, personCache, + True, pubKey) + print('actor update result: ' + str(actorUpdate)) + assert actorUpdate + + # load alice actor + print('Loading actor: ' + actorFilename) + actorJson = loadJson(actorFilename) + assert actorJson + if len(actorJson['attachment']) == 0: + print("actorJson['attachment'] has no contents") + assert len(actorJson['attachment']) > 0 + propertyFound = False + for propertyValue in actorJson['attachment']: + if propertyValue['name'] == 'PGP': + print('PGP property set within attachment') + assert pubKey in propertyValue['value'] + propertyFound = True + assert propertyFound + + # stop the server + thrAlice.kill() + thrAlice.join() + assert thrAlice.is_alive() is False + + os.chdir(baseDir) + if os.path.isdir(baseDir + '/.tests'): + shutil.rmtree(baseDir + '/.tests') + + def runAllTests(): print('Running tests...') testFunctions() From 133b77386a8df8250254f888f4c9d1accf1e4f72 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 17 Mar 2021 20:22:27 +0000 Subject: [PATCH 06/87] Missing id --- pgp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgp.py b/pgp.py index f369727c2..4e918b5ff 100644 --- a/pgp.py +++ b/pgp.py @@ -425,7 +425,7 @@ def _pgpLocalPublicKeyId() -> str: def _pgpLocalPublicKey() -> str: """Gets the local pgp public key """ - keyId = _pgpLocalPublicKey() + keyId = _pgpLocalPublicKeyId() if not keyId: return None cmdStr = "gpg --armor --export " + keyId From 9f872d8b61411c2fb236d91658e55f67365b98b8 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 17 Mar 2021 20:23:44 +0000 Subject: [PATCH 07/87] Decode --- pgp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgp.py b/pgp.py index 4e918b5ff..ab717defe 100644 --- a/pgp.py +++ b/pgp.py @@ -419,7 +419,7 @@ def _pgpLocalPublicKeyId() -> str: return None if len(result) < 5: return None - return result.replace('"', '').strip() + return result.decode('utf-8').replace('"', '').strip() def _pgpLocalPublicKey() -> str: From c9d2a80cdeee18b71ccc7264650f1e0612c8ed98 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 17 Mar 2021 20:24:42 +0000 Subject: [PATCH 08/87] Decode --- pgp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgp.py b/pgp.py index ab717defe..9398c75b7 100644 --- a/pgp.py +++ b/pgp.py @@ -436,7 +436,7 @@ def _pgpLocalPublicKey() -> str: return None if not result: return None - return extractPGPPublicKey(result) + return extractPGPPublicKey(result.decode('utf-8')) def pgpPublicKeyUpload(baseDir: str, session, From 364a27014d51b8db26de5d69ba707e137a7de3d0 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 17 Mar 2021 20:27:08 +0000 Subject: [PATCH 09/87] Avoid print --- person.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/person.py b/person.py index a9b855423..d56c0fd5f 100644 --- a/person.py +++ b/person.py @@ -1235,7 +1235,7 @@ def getActorJson(handle: str, http: bool, gnunet: bool, getJson(session, personUrl, asHeader, None, debug, __version__, httpPrefix, None, 20, quiet) if personJson: - if not quiet or debug: + if not quiet: pprint(personJson) return personJson else: @@ -1246,7 +1246,7 @@ def getActorJson(handle: str, http: bool, gnunet: bool, personJson = \ getJson(session, personUrl, asHeader, None, debug, __version__, httpPrefix, None) - if not quiet or debug: + if not quiet: if personJson: print('getActorJson returned actor') pprint(personJson) From d5f82c568d95293cec2bc09a6a8b438500ce847c Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 17 Mar 2021 20:31:55 +0000 Subject: [PATCH 10/87] Function arguments --- epicyon.py | 2 +- pgp.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/epicyon.py b/epicyon.py index 0b51d50f5..674c1f5d6 100644 --- a/epicyon.py +++ b/epicyon.py @@ -1413,7 +1413,7 @@ if args.migrations: sys.exit() if args.actor: - getActorJson(args.actor, args.http, args.gnunet, False) + getActorJson(args.actor, args.http, args.gnunet, debug) sys.exit() if args.followers: diff --git a/pgp.py b/pgp.py index 9398c75b7..90af8d1b8 100644 --- a/pgp.py +++ b/pgp.py @@ -337,7 +337,7 @@ def _getPGPPublicKeyFromActor(handle: str, actorJson=None) -> str: public key specified """ if not actorJson: - actorJson = getActorJson(handle, False, False, True) + actorJson = getActorJson(handle, False, False, False, True) if not actorJson: return None if not actorJson.get('attachment'): @@ -475,7 +475,7 @@ def pgpPublicKeyUpload(baseDir: str, session, if debug: print('Getting actor for ' + handle) - actorJson = getActorJson(handle, False, False, True) + actorJson = getActorJson(handle, False, False, debug, True) if not actorJson: if debug: print('No actor returned for ' + handle) From eb9135ac3e58923fcf12fe2c5f498a28b1b4c60c Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 17 Mar 2021 21:17:27 +0000 Subject: [PATCH 11/87] Another long lines failure case --- content.py | 4 ++++ tests.py | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/content.py b/content.py index cd10d9d73..baabd8c9e 100644 --- a/content.py +++ b/content.py @@ -643,6 +643,8 @@ def removeLongWords(content: str, maxWordLength: int, if wordStr not in longWordsList: longWordsList.append(wordStr) for wordStr in longWordsList: + if wordStr.startswith('

'): + wordStr = wordStr.replace('

', '') if wordStr.startswith('<'): continue if len(wordStr) == 76: @@ -678,6 +680,8 @@ def removeLongWords(content: str, maxWordLength: int, continue if '<' in wordStr: replaceWord = wordStr.split('<', 1)[0] + # if len(replaceWord) > maxWordLength: + # replaceWord = replaceWord[:maxWordLength] content = content.replace(wordStr, replaceWord) wordStr = replaceWord if '/' in wordStr: diff --git a/tests.py b/tests.py index 6a232dc71..7b187bf6b 100644 --- a/tests.py +++ b/tests.py @@ -1911,6 +1911,22 @@ def testActorParsing(): def testWebLinks(): print('testWebLinks') + exampleText = \ + "

Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + \ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + \ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + \ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + \ + " #turbot #haddock

" + resultText = removeLongWords(exampleText, 40, []) + assert resultText == "

Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + \ + " #turbot " + \ + "#haddock

" + exampleText = \ '

@foo Some ' + \ From dacab2b638aa33b6fb15b0ce4a3b26ffae6eb670 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 17 Mar 2021 21:23:52 +0000 Subject: [PATCH 12/87] Debug --- pgp.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pgp.py b/pgp.py index 90af8d1b8..50ad0cc5c 100644 --- a/pgp.py +++ b/pgp.py @@ -579,14 +579,15 @@ def pgpPublicKeyUpload(baseDir: str, session, 'Content-type': 'application/json', 'Authorization': authHeader } - tries = 0 quiet = not debug + tries = 0 while tries < 4: postResult = \ postJson(session, actorUpdate, [], inboxUrl, headers, 30, quiet) if postResult: break + print('tries = ' + str(tries)) time.sleep(2) tries += 1 From dd365dbc6045fa3c674bee1aa5a13afcd2215159 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 17 Mar 2021 21:26:58 +0000 Subject: [PATCH 13/87] Debug --- pgp.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pgp.py b/pgp.py index 50ad0cc5c..247bfe047 100644 --- a/pgp.py +++ b/pgp.py @@ -582,13 +582,12 @@ def pgpPublicKeyUpload(baseDir: str, session, quiet = not debug tries = 0 while tries < 4: + print('tries = ' + str(tries)) postResult = \ postJson(session, actorUpdate, [], inboxUrl, - headers, 30, quiet) + headers, 5, quiet) if postResult: break - print('tries = ' + str(tries)) - time.sleep(2) tries += 1 if postResult is None: From df166c63b2f8dd4c1d181b632722dd4faa32baa1 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 17 Mar 2021 21:32:30 +0000 Subject: [PATCH 14/87] Shorter timeout --- pgp.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pgp.py b/pgp.py index 247bfe047..8409a7007 100644 --- a/pgp.py +++ b/pgp.py @@ -582,7 +582,6 @@ def pgpPublicKeyUpload(baseDir: str, session, quiet = not debug tries = 0 while tries < 4: - print('tries = ' + str(tries)) postResult = \ postJson(session, actorUpdate, [], inboxUrl, headers, 5, quiet) From c5b273512b6110a25223b7ff044a8818c6707bc9 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 17 Mar 2021 21:46:52 +0000 Subject: [PATCH 15/87] Simplify --- outbox.py | 15 ++++++++------- pgp.py | 1 - 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/outbox.py b/outbox.py index 038e069eb..7c8a822dd 100644 --- a/outbox.py +++ b/outbox.py @@ -127,24 +127,25 @@ def _outboxPersonReceiveUpdate(recentPostsCache: {}, continue if 'attachment' in actorJson: found = False + ctr = 0 for propertyValue in actorJson['attachment']: if propertyValue != 'PropertyValue': + ctr += 1 continue if propertyValue['name'] == newPropertyValue['name']: - if propertyValue['value'] != \ - newPropertyValue['value']: - propertyValue['value'] = \ - newPropertyValue['value'] - actorChanged = True - found = True + found = True break + ctr += 1 if not found: actorJson['attachment'].append({ "name": newPropertyValue['name'], "type": "PropertyValue", "value": newPropertyValue['value'] }) - actorChanged = True + else: + actorJson['attachment'][ctr]['value'] = \ + newPropertyValue['value'] + actorChanged = True # save actor to file if actorChanged: saveJson(actorJson, actorFilename) diff --git a/pgp.py b/pgp.py index 8409a7007..9b18c8cc3 100644 --- a/pgp.py +++ b/pgp.py @@ -7,7 +7,6 @@ __email__ = "bob@freedombone.net" __status__ = "Production" import os -import time import subprocess from pathlib import Path from person import getActorJson From fe912d36555999aa9607f55da467605f098470fb Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 17 Mar 2021 21:53:42 +0000 Subject: [PATCH 16/87] Increase max message length, for uploading actor --- daemon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/daemon.py b/daemon.py index 92bf42014..c7edcc342 100644 --- a/daemon.py +++ b/daemon.py @@ -14546,8 +14546,8 @@ def runDaemon(brochMode: bool, # max POST size of 30M httpd.maxPostLength = 1024 * 1024 * 30 httpd.maxMediaSize = httpd.maxPostLength - # Maximum text length is 32K - enough for a blog post - httpd.maxMessageLength = 32000 + # Maximum text length is 64K - enough for a blog post + httpd.maxMessageLength = 64000 # Maximum overall number of posts per box httpd.maxPostsInBox = 32000 httpd.domain = domain From 7a91b9b71a1e22229cd6449c8084428a35b8d24c Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 17 Mar 2021 22:11:38 +0000 Subject: [PATCH 17/87] Name the actor --- posts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/posts.py b/posts.py index 7da14e9bd..9de97b77c 100644 --- a/posts.py +++ b/posts.py @@ -259,7 +259,7 @@ def getPersonBox(baseDir: str, session, wfRequest: {}, personJson = getJson(session, personUrl, asHeader, None, debug, projectVersion, httpPrefix, domain) if not personJson: - print('Unable to get actor') + print('Unable to get actor for ' + personUrl) return None, None, None, None, None, None, None boxJson = None if not personJson.get(boxName): From a0b20fbf64eefc28ca83786b5c54da65674c3dd0 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 17 Mar 2021 22:36:46 +0000 Subject: [PATCH 18/87] Changing existing attachments --- outbox.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/outbox.py b/outbox.py index 7c8a822dd..1296ccb9f 100644 --- a/outbox.py +++ b/outbox.py @@ -132,7 +132,14 @@ def _outboxPersonReceiveUpdate(recentPostsCache: {}, if propertyValue != 'PropertyValue': ctr += 1 continue - if propertyValue['name'] == newPropertyValue['name']: + if propertyValue['name'] != newPropertyValue['name']: + ctr += 1 + continue + else: + if propertyValue['value'] != newPropertyValue['value']: + actorJson['attachment'][ctr]['value'] = \ + newPropertyValue['value'] + actorChanged = True found = True break ctr += 1 @@ -142,10 +149,7 @@ def _outboxPersonReceiveUpdate(recentPostsCache: {}, "type": "PropertyValue", "value": newPropertyValue['value'] }) - else: - actorJson['attachment'][ctr]['value'] = \ - newPropertyValue['value'] - actorChanged = True + actorChanged = True # save actor to file if actorChanged: saveJson(actorJson, actorFilename) From d3a42f862d624c920604a0fb4fd90bd1bc5f886f Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 17 Mar 2021 22:50:17 +0000 Subject: [PATCH 19/87] Use attachment index --- outbox.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/outbox.py b/outbox.py index 1296ccb9f..11b3f2dc4 100644 --- a/outbox.py +++ b/outbox.py @@ -127,22 +127,21 @@ def _outboxPersonReceiveUpdate(recentPostsCache: {}, continue if 'attachment' in actorJson: found = False - ctr = 0 - for propertyValue in actorJson['attachment']: - if propertyValue != 'PropertyValue': - ctr += 1 + for attachIdx in range(len(actorJson['attachment'])): + if actorJson['attachment'][attachIdx]['type'] != \ + 'PropertyValue': continue - if propertyValue['name'] != newPropertyValue['name']: - ctr += 1 + if actorJson['attachment'][attachIdx]['name'] != \ + newPropertyValue['name']: continue else: - if propertyValue['value'] != newPropertyValue['value']: - actorJson['attachment'][ctr]['value'] = \ + if actorJson['attachment'][attachIdx]['value'] != \ + newPropertyValue['value']: + actorJson['attachment'][attachIdx]['value'] = \ newPropertyValue['value'] actorChanged = True found = True break - ctr += 1 if not found: actorJson['attachment'].append({ "name": newPropertyValue['name'], From 4804070fd164ba4e947c3fbd5aa5049ce4a3ddf8 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 18 Mar 2021 10:01:01 +0000 Subject: [PATCH 20/87] Improve c2s debug messages --- announce.py | 11 ++++++----- availability.py | 13 +++++++------ delete.py | 13 +++++++------ follow.py | 28 +++++++++++++++------------- like.py | 24 ++++++++++++------------ posts.py | 40 +++++++++++++++++++++------------------- roles.py | 13 +++++++------ shares.py | 28 +++++++++++++++------------- skills.py | 13 +++++++------ 9 files changed, 97 insertions(+), 86 deletions(-) diff --git a/announce.py b/announce.py index 60679e4de..d07f855a1 100644 --- a/announce.py +++ b/announce.py @@ -225,8 +225,8 @@ def sendAnnounceViaServer(baseDir: str, session, print('DEBUG: announce webfinger failed for ' + handle) return 1 if not isinstance(wfRequest, dict): - print('WARN: Webfinger for ' + handle + ' did not return a dict. ' + - str(wfRequest)) + print('WARN: announce webfinger for ' + handle + + ' did not return a dict. ' + str(wfRequest)) return 1 postToBox = 'outbox' @@ -242,11 +242,12 @@ def sendAnnounceViaServer(baseDir: str, session, if not inboxUrl: if debug: - print('DEBUG: No ' + postToBox + ' was found for ' + handle) + print('DEBUG: announce no ' + postToBox + + ' was found for ' + handle) return 3 if not fromPersonId: if debug: - print('DEBUG: No actor was found for ' + handle) + print('DEBUG: announce no actor was found for ' + handle) return 4 authHeader = createBasicAuthHeader(fromNickname, password) @@ -259,7 +260,7 @@ def sendAnnounceViaServer(baseDir: str, session, postResult = postJson(session, newAnnounceJson, [], inboxUrl, headers, 30, True) if not postResult: - print('WARN: Announce not posted') + print('WARN: announce not posted') if debug: print('DEBUG: c2s POST announce success') diff --git a/availability.py b/availability.py index 5b88c4103..7b8196448 100644 --- a/availability.py +++ b/availability.py @@ -108,11 +108,11 @@ def sendAvailabilityViaServer(baseDir: str, session, domain, projectVersion, debug) if not wfRequest: if debug: - print('DEBUG: announce webfinger failed for ' + handle) + print('DEBUG: availability webfinger failed for ' + handle) return 1 if not isinstance(wfRequest, dict): - print('WARN: Webfinger for ' + handle + ' did not return a dict. ' + - str(wfRequest)) + print('WARN: availability webfinger for ' + handle + + ' did not return a dict. ' + str(wfRequest)) return 1 postToBox = 'outbox' @@ -127,11 +127,12 @@ def sendAvailabilityViaServer(baseDir: str, session, if not inboxUrl: if debug: - print('DEBUG: No ' + postToBox + ' was found for ' + handle) + print('DEBUG: availability no ' + postToBox + + ' was found for ' + handle) return 3 if not fromPersonId: if debug: - print('DEBUG: No actor was found for ' + handle) + print('DEBUG: availability no actor was found for ' + handle) return 4 authHeader = createBasicAuthHeader(nickname, password) @@ -144,7 +145,7 @@ def sendAvailabilityViaServer(baseDir: str, session, postResult = postJson(session, newAvailabilityJson, [], inboxUrl, headers, 30, True) if not postResult: - print('WARN: failed to post availability') + print('WARN: availability failed to post') if debug: print('DEBUG: c2s POST availability success') diff --git a/delete.py b/delete.py index 737f574e8..d904c5275 100644 --- a/delete.py +++ b/delete.py @@ -58,11 +58,11 @@ def sendDeleteViaServer(baseDir: str, session, fromDomain, projectVersion, debug) if not wfRequest: if debug: - print('DEBUG: announce webfinger failed for ' + handle) + print('DEBUG: delete webfinger failed for ' + handle) return 1 if not isinstance(wfRequest, dict): - print('WARN: Webfinger for ' + handle + ' did not return a dict. ' + - str(wfRequest)) + print('WARN: delete webfinger for ' + handle + + ' did not return a dict. ' + str(wfRequest)) return 1 postToBox = 'outbox' @@ -76,11 +76,12 @@ def sendDeleteViaServer(baseDir: str, session, if not inboxUrl: if debug: - print('DEBUG: No ' + postToBox + ' was found for ' + handle) + print('DEBUG: delete no ' + postToBox + + ' was found for ' + handle) return 3 if not fromPersonId: if debug: - print('DEBUG: No actor was found for ' + handle) + print('DEBUG: delete no actor was found for ' + handle) return 4 authHeader = createBasicAuthHeader(fromNickname, password) @@ -94,7 +95,7 @@ def sendDeleteViaServer(baseDir: str, session, postJson(session, newDeleteJson, [], inboxUrl, headers, 30, True) if not postResult: if debug: - print('DEBUG: POST announce failed for c2s to ' + inboxUrl) + print('DEBUG: POST delete failed for c2s to ' + inboxUrl) return 5 if debug: diff --git a/follow.py b/follow.py index 6da40f642..ab2b5d595 100644 --- a/follow.py +++ b/follow.py @@ -992,11 +992,11 @@ def sendFollowRequestViaServer(baseDir: str, session, fromDomain, projectVersion, debug) if not wfRequest: if debug: - print('DEBUG: announce webfinger failed for ' + handle) + print('DEBUG: follow request webfinger failed for ' + handle) return 1 if not isinstance(wfRequest, dict): - print('WARN: Webfinger for ' + handle + ' did not return a dict. ' + - str(wfRequest)) + print('WARN: follow request Webfinger for ' + handle + + ' did not return a dict. ' + str(wfRequest)) return 1 postToBox = 'outbox' @@ -1010,11 +1010,12 @@ def sendFollowRequestViaServer(baseDir: str, session, if not inboxUrl: if debug: - print('DEBUG: No ' + postToBox + ' was found for ' + handle) + print('DEBUG: follow request no ' + postToBox + + ' was found for ' + handle) return 3 if not fromPersonId: if debug: - print('DEBUG: No actor was found for ' + handle) + print('DEBUG: follow request no actor was found for ' + handle) return 4 authHeader = createBasicAuthHeader(fromNickname, password) @@ -1028,11 +1029,11 @@ def sendFollowRequestViaServer(baseDir: str, session, postJson(session, newFollowJson, [], inboxUrl, headers, 30, True) if not postResult: if debug: - print('DEBUG: POST follow failed for c2s to ' + inboxUrl) + print('DEBUG: POST follow request failed for c2s to ' + inboxUrl) return 5 if debug: - print('DEBUG: c2s POST follow success') + print('DEBUG: c2s POST follow request success') return newFollowJson @@ -1081,11 +1082,11 @@ def sendUnfollowRequestViaServer(baseDir: str, session, fromDomain, projectVersion, debug) if not wfRequest: if debug: - print('DEBUG: announce webfinger failed for ' + handle) + print('DEBUG: unfollow webfinger failed for ' + handle) return 1 if not isinstance(wfRequest, dict): - print('WARN: Webfinger for ' + handle + ' did not return a dict. ' + - str(wfRequest)) + print('WARN: unfollow webfinger for ' + handle + + ' did not return a dict. ' + str(wfRequest)) return 1 postToBox = 'outbox' @@ -1102,11 +1103,12 @@ def sendUnfollowRequestViaServer(baseDir: str, session, if not inboxUrl: if debug: - print('DEBUG: No ' + postToBox + ' was found for ' + handle) + print('DEBUG: unfollow no ' + postToBox + + ' was found for ' + handle) return 3 if not fromPersonId: if debug: - print('DEBUG: No actor was found for ' + handle) + print('DEBUG: unfollow no actor was found for ' + handle) return 4 authHeader = createBasicAuthHeader(fromNickname, password) @@ -1120,7 +1122,7 @@ def sendUnfollowRequestViaServer(baseDir: str, session, postJson(session, unfollowJson, [], inboxUrl, headers, 30, True) if not postResult: if debug: - print('DEBUG: POST announce failed for c2s to ' + inboxUrl) + print('DEBUG: POST unfollow failed for c2s to ' + inboxUrl) return 5 if debug: diff --git a/like.py b/like.py index acf243f52..2bc561de5 100644 --- a/like.py +++ b/like.py @@ -170,11 +170,11 @@ def sendLikeViaServer(baseDir: str, session, fromDomain, projectVersion, debug) if not wfRequest: if debug: - print('DEBUG: announce webfinger failed for ' + handle) + print('DEBUG: like webfinger failed for ' + handle) return 1 if not isinstance(wfRequest, dict): - print('WARN: Webfinger for ' + handle + ' did not return a dict. ' + - str(wfRequest)) + print('WARN: like webfinger for ' + handle + + ' did not return a dict. ' + str(wfRequest)) return 1 postToBox = 'outbox' @@ -189,11 +189,11 @@ def sendLikeViaServer(baseDir: str, session, if not inboxUrl: if debug: - print('DEBUG: No ' + postToBox + ' was found for ' + handle) + print('DEBUG: like no ' + postToBox + ' was found for ' + handle) return 3 if not fromPersonId: if debug: - print('DEBUG: No actor was found for ' + handle) + print('DEBUG: like no actor was found for ' + handle) return 4 authHeader = createBasicAuthHeader(fromNickname, password) @@ -207,7 +207,7 @@ def sendLikeViaServer(baseDir: str, session, headers, 30, True) if not postResult: if debug: - print('WARN: POST announce failed for c2s to ' + inboxUrl) + print('WARN: POST like failed for c2s to ' + inboxUrl) return 5 if debug: @@ -251,11 +251,11 @@ def sendUndoLikeViaServer(baseDir: str, session, fromDomain, projectVersion, debug) if not wfRequest: if debug: - print('DEBUG: announce webfinger failed for ' + handle) + print('DEBUG: unlike webfinger failed for ' + handle) return 1 if not isinstance(wfRequest, dict): if debug: - print('WARN: Webfinger for ' + handle + + print('WARN: unlike webfinger for ' + handle + ' did not return a dict. ' + str(wfRequest)) return 1 @@ -271,11 +271,11 @@ def sendUndoLikeViaServer(baseDir: str, session, if not inboxUrl: if debug: - print('DEBUG: No ' + postToBox + ' was found for ' + handle) + print('DEBUG: unlike no ' + postToBox + ' was found for ' + handle) return 3 if not fromPersonId: if debug: - print('DEBUG: No actor was found for ' + handle) + print('DEBUG: unlike no actor was found for ' + handle) return 4 authHeader = createBasicAuthHeader(fromNickname, password) @@ -289,11 +289,11 @@ def sendUndoLikeViaServer(baseDir: str, session, headers, 30, True) if not postResult: if debug: - print('WARN: POST announce failed for c2s to ' + inboxUrl) + print('WARN: POST unlike failed for c2s to ' + inboxUrl) return 5 if debug: - print('DEBUG: c2s POST undo like success') + print('DEBUG: c2s POST unlike success') return newUndoLikeJson diff --git a/posts.py b/posts.py index 9de97b77c..cf6c63d3b 100644 --- a/posts.py +++ b/posts.py @@ -2056,11 +2056,11 @@ def sendPostViaServer(projectVersion: str, fromDomain, projectVersion, debug) if not wfRequest: if debug: - print('DEBUG: webfinger failed for ' + handle) + print('DEBUG: post webfinger failed for ' + handle) return 1 if not isinstance(wfRequest, dict): - print('WARN: Webfinger for ' + handle + ' did not return a dict. ' + - str(wfRequest)) + print('WARN: post webfinger for ' + handle + + ' did not return a dict. ' + str(wfRequest)) return 1 postToBox = 'outbox' @@ -2078,11 +2078,12 @@ def sendPostViaServer(projectVersion: str, 82796) if not inboxUrl: if debug: - print('DEBUG: No ' + postToBox + ' was found for ' + handle) + print('DEBUG: post no ' + postToBox + + ' was found for ' + handle) return 3 if not fromPersonId: if debug: - print('DEBUG: No actor was found for ' + handle) + print('DEBUG: post no actor was found for ' + handle) return 4 # Get the json for the c2s post, not saving anything to file @@ -2131,7 +2132,7 @@ def sendPostViaServer(projectVersion: str, inboxUrl, headers) if not postResult: if debug: - print('DEBUG: Failed to upload image') + print('DEBUG: post failed to upload image') # return 9 headers = { @@ -4156,11 +4157,11 @@ def sendBlockViaServer(baseDir: str, session, fromDomain, projectVersion, debug) if not wfRequest: if debug: - print('DEBUG: announce webfinger failed for ' + handle) + print('DEBUG: block webfinger failed for ' + handle) return 1 if not isinstance(wfRequest, dict): - print('WARN: Webfinger for ' + handle + ' did not return a dict. ' + - str(wfRequest)) + print('WARN: block Webfinger for ' + handle + + ' did not return a dict. ' + str(wfRequest)) return 1 postToBox = 'outbox' @@ -4175,11 +4176,11 @@ def sendBlockViaServer(baseDir: str, session, if not inboxUrl: if debug: - print('DEBUG: No ' + postToBox + ' was found for ' + handle) + print('DEBUG: block no ' + postToBox + ' was found for ' + handle) return 3 if not fromPersonId: if debug: - print('DEBUG: No actor was found for ' + handle) + print('DEBUG: block no actor was found for ' + handle) return 4 authHeader = createBasicAuthHeader(fromNickname, password) @@ -4192,7 +4193,7 @@ def sendBlockViaServer(baseDir: str, session, postResult = postJson(session, newBlockJson, [], inboxUrl, headers, 30, True) if not postResult: - print('WARN: Unable to post block') + print('WARN: block unable to post') if debug: print('DEBUG: c2s POST block success') @@ -4240,11 +4241,11 @@ def sendUndoBlockViaServer(baseDir: str, session, fromDomain, projectVersion, debug) if not wfRequest: if debug: - print('DEBUG: announce webfinger failed for ' + handle) + print('DEBUG: unblock webfinger failed for ' + handle) return 1 if not isinstance(wfRequest, dict): - print('WARN: Webfinger for ' + handle + ' did not return a dict. ' + - str(wfRequest)) + print('WARN: unblock webfinger for ' + handle + + ' did not return a dict. ' + str(wfRequest)) return 1 postToBox = 'outbox' @@ -4258,11 +4259,12 @@ def sendUndoBlockViaServer(baseDir: str, session, if not inboxUrl: if debug: - print('DEBUG: No ' + postToBox + ' was found for ' + handle) + print('DEBUG: unblock no ' + postToBox + + ' was found for ' + handle) return 3 if not fromPersonId: if debug: - print('DEBUG: No actor was found for ' + handle) + print('DEBUG: unblock no actor was found for ' + handle) return 4 authHeader = createBasicAuthHeader(fromNickname, password) @@ -4275,10 +4277,10 @@ def sendUndoBlockViaServer(baseDir: str, session, postResult = postJson(session, newBlockJson, [], inboxUrl, headers, 30, True) if not postResult: - print('WARN: Unable to post block') + print('WARN: unblock unable to post') if debug: - print('DEBUG: c2s POST block success') + print('DEBUG: c2s POST unblock success') return newBlockJson diff --git a/roles.py b/roles.py index 218ff7d3a..f198fe7f0 100644 --- a/roles.py +++ b/roles.py @@ -313,11 +313,11 @@ def sendRoleViaServer(baseDir: str, session, delegatorDomain, projectVersion, debug) if not wfRequest: if debug: - print('DEBUG: announce webfinger failed for ' + handle) + print('DEBUG: role webfinger failed for ' + handle) return 1 if not isinstance(wfRequest, dict): - print('WARN: Webfinger for ' + handle + ' did not return a dict. ' + - str(wfRequest)) + print('WARN: role webfinger for ' + handle + + ' did not return a dict. ' + str(wfRequest)) return 1 postToBox = 'outbox' @@ -334,11 +334,12 @@ def sendRoleViaServer(baseDir: str, session, if not inboxUrl: if debug: - print('DEBUG: No ' + postToBox + ' was found for ' + handle) + print('DEBUG: role no ' + postToBox + + ' was found for ' + handle) return 3 if not fromPersonId: if debug: - print('DEBUG: No actor was found for ' + handle) + print('DEBUG: role no actor was found for ' + handle) return 4 authHeader = createBasicAuthHeader(delegatorNickname, password) @@ -352,7 +353,7 @@ def sendRoleViaServer(baseDir: str, session, postJson(session, newRoleJson, [], inboxUrl, headers, 30, True) if not postResult: if debug: - print('DEBUG: POST announce failed for c2s to ' + inboxUrl) + print('DEBUG: POST role failed for c2s to ' + inboxUrl) # return 5 if debug: diff --git a/shares.py b/shares.py index 239f79a70..ecf59dfc1 100644 --- a/shares.py +++ b/shares.py @@ -361,11 +361,11 @@ def sendShareViaServer(baseDir, session, fromDomain, projectVersion, debug) if not wfRequest: if debug: - print('DEBUG: announce webfinger failed for ' + handle) + print('DEBUG: share webfinger failed for ' + handle) return 1 if not isinstance(wfRequest, dict): - print('WARN: Webfinger for ' + handle + ' did not return a dict. ' + - str(wfRequest)) + print('WARN: share webfinger for ' + handle + + ' did not return a dict. ' + str(wfRequest)) return 1 postToBox = 'outbox' @@ -381,11 +381,12 @@ def sendShareViaServer(baseDir, session, if not inboxUrl: if debug: - print('DEBUG: No ' + postToBox + ' was found for ' + handle) + print('DEBUG: share no ' + postToBox + + ' was found for ' + handle) return 3 if not fromPersonId: if debug: - print('DEBUG: No actor was found for ' + handle) + print('DEBUG: share no actor was found for ' + handle) return 4 authHeader = createBasicAuthHeader(fromNickname, password) @@ -409,7 +410,7 @@ def sendShareViaServer(baseDir, session, postJson(session, newShareJson, [], inboxUrl, headers, 30, True) if not postResult: if debug: - print('DEBUG: POST announce failed for c2s to ' + inboxUrl) + print('DEBUG: POST share failed for c2s to ' + inboxUrl) # return 5 if debug: @@ -460,11 +461,11 @@ def sendUndoShareViaServer(baseDir: str, session, fromDomain, projectVersion, debug) if not wfRequest: if debug: - print('DEBUG: announce webfinger failed for ' + handle) + print('DEBUG: unshare webfinger failed for ' + handle) return 1 if not isinstance(wfRequest, dict): - print('WARN: Webfinger for ' + handle + ' did not return a dict. ' + - str(wfRequest)) + print('WARN: unshare webfinger for ' + handle + + ' did not return a dict. ' + str(wfRequest)) return 1 postToBox = 'outbox' @@ -480,11 +481,12 @@ def sendUndoShareViaServer(baseDir: str, session, if not inboxUrl: if debug: - print('DEBUG: No '+postToBox+' was found for ' + handle) + print('DEBUG: unshare no ' + postToBox + + ' was found for ' + handle) return 3 if not fromPersonId: if debug: - print('DEBUG: No actor was found for ' + handle) + print('DEBUG: unshare no actor was found for ' + handle) return 4 authHeader = createBasicAuthHeader(fromNickname, password) @@ -499,11 +501,11 @@ def sendUndoShareViaServer(baseDir: str, session, headers, 30, True) if not postResult: if debug: - print('DEBUG: POST announce failed for c2s to ' + inboxUrl) + print('DEBUG: POST unshare failed for c2s to ' + inboxUrl) # return 5 if debug: - print('DEBUG: c2s POST undo share success') + print('DEBUG: c2s POST unshare success') return undoShareJson diff --git a/skills.py b/skills.py index 267c43f37..0609bfd5a 100644 --- a/skills.py +++ b/skills.py @@ -126,11 +126,11 @@ def sendSkillViaServer(baseDir: str, session, nickname: str, password: str, domain, projectVersion, debug) if not wfRequest: if debug: - print('DEBUG: announce webfinger failed for ' + handle) + print('DEBUG: skill webfinger failed for ' + handle) return 1 if not isinstance(wfRequest, dict): - print('WARN: Webfinger for ' + handle + ' did not return a dict. ' + - str(wfRequest)) + print('WARN: skill webfinger for ' + handle + + ' did not return a dict. ' + str(wfRequest)) return 1 postToBox = 'outbox' @@ -145,11 +145,12 @@ def sendSkillViaServer(baseDir: str, session, nickname: str, password: str, if not inboxUrl: if debug: - print('DEBUG: No ' + postToBox + ' was found for ' + handle) + print('DEBUG: skill no ' + postToBox + + ' was found for ' + handle) return 3 if not fromPersonId: if debug: - print('DEBUG: No actor was found for ' + handle) + print('DEBUG: skill no actor was found for ' + handle) return 4 authHeader = createBasicAuthHeader(nickname, password) @@ -164,7 +165,7 @@ def sendSkillViaServer(baseDir: str, session, nickname: str, password: str, headers, 30, True) if not postResult: if debug: - print('DEBUG: POST announce failed for c2s to ' + inboxUrl) + print('DEBUG: POST skill failed for c2s to ' + inboxUrl) # return 5 if debug: From 6f0d4ede533203629cf94c5c2dc9c1cf9d720b96 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 18 Mar 2021 11:03:39 +0000 Subject: [PATCH 21/87] Authenticated c2s timeline options --- README_commandline.md | 10 +++++++++ epicyon.py | 48 +++++++++++++++++++++++++++++++++++++++++++ posts.py | 36 ++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+) diff --git a/README_commandline.md b/README_commandline.md index 62c28f28a..c6b392aec 100644 --- a/README_commandline.md +++ b/README_commandline.md @@ -137,6 +137,16 @@ If you want to view the raw JSON: python3 epicyon.py --postsraw nickname@domain ``` +## Getting the JSON for your timelines + +The **--posts** option applies for any ActivityPub compatible fediverse account with visible public posts. You can also use an authenticated version to obtain the paginated JSON for your inbox, outbox, direct messages, etc. + +``` bash +python3 epicyon.py --nickname [yournick] --domain [yourdomain] --box [inbox|outbox|dm] --page [number] --password [yourpassword] +``` + +You could use this to make your own c2s client, or create your own notification system. + ## Listing referenced domains To list the domains referenced in public posts: diff --git a/epicyon.py b/epicyon.py index 674c1f5d6..3052c1f06 100644 --- a/epicyon.py +++ b/epicyon.py @@ -22,6 +22,7 @@ from person import deactivateAccount from skills import setSkillLevel from roles import setRole from webfinger import webfingerHandle +from posts import c2sBoxJson from posts import downloadFollowCollection from posts import getPublicPostDomains from posts import getPublicPostDomainsBlocked @@ -405,6 +406,13 @@ parser.add_argument("--allowdeletion", type=str2bool, nargs='?', parser.add_argument('--repeat', '--announce', dest='announce', type=str, default=None, help='Announce/repeat a url') +parser.add_argument('--box', type=str, + default=None, + help='Returns the json for a given timeline, ' + + 'with authentication') +parser.add_argument('--page', '--pageNumber', dest='pageNumber', type=int, + default=1, + help='Page number when using the --box option') parser.add_argument('--favorite', '--like', dest='like', type=str, default=None, help='Like a url') parser.add_argument('--undolike', '--unlike', dest='undolike', type=str, @@ -1112,6 +1120,46 @@ if args.announce: time.sleep(1) sys.exit() +if args.box: + if not domain: + print('Specify a domain with the --domain option') + sys.exit() + + if not args.nickname: + print('Specify a nickname with the --nickname option') + sys.exit() + + if not args.password: + args.password = getpass.getpass('Password: ') + if not args.password: + print('Specify a password with the --password option') + sys.exit() + args.password = args.password.replace('\n', '') + + proxyType = None + if args.tor or domain.endswith('.onion'): + proxyType = 'tor' + if domain.endswith('.onion'): + args.port = 80 + elif args.i2p or domain.endswith('.i2p'): + proxyType = 'i2p' + if domain.endswith('.i2p'): + args.port = 80 + elif args.gnunet: + proxyType = 'gnunet' + + session = createSession(proxyType) + boxJson = c2sBoxJson(baseDir, session, + args.nickname, args.password, + domain, port, httpPrefix, + args.box, args.pageNumber, + args.debug) + if boxJson: + pprint(boxJson) + else: + print('Box not found: ' + args.box) + sys.exit() + if args.itemName: if not args.password: args.password = getpass.getpass('Password: ') diff --git a/posts.py b/posts.py index cf6c63d3b..af0505a7b 100644 --- a/posts.py +++ b/posts.py @@ -4307,3 +4307,39 @@ def postIsMuted(baseDir: str, nickname: str, domain: str, if os.path.isfile(muteFilename): return True return False + + +def c2sBoxJson(baseDir: str, session, + nickname: str, password: str, + domain: str, port: int, + httpPrefix: str, + boxName: str, pageNumber: int, + debug: bool) -> {}: + """C2S Authenticated GET of posts for a timeline + """ + if not session: + print('WARN: No session for c2sBoxJson') + return None + + domainFull = getFullDomain(domain, port) + actor = httpPrefix + '://' + domainFull + '/users/' + nickname + + authHeader = createBasicAuthHeader(nickname, password) + + profileStr = 'https://www.w3.org/ns/activitystreams' + headers = { + 'host': domain, + 'Content-type': 'application/json', + 'Authorization': authHeader, + 'Accept': 'application/ld+json; profile="' + profileStr + '"' + } + + # GET json + url = actor + '/' + boxName + '?page=' + str(pageNumber) + boxJson = getJson(session, url, headers, None, + debug, __version__, httpPrefix, None) + + if boxJson is not None and debug: + print('DEBUG: GET c2sBoxJson success') + + return boxJson From b677ff544ac07c6dfc3d2a9e04671ace7a1377a2 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 18 Mar 2021 16:53:32 +0000 Subject: [PATCH 22/87] Re-engineering desktop client to be more ActivityPub compatible --- desktop_client.py | 725 ++++++++++++++++++++++------------------------ 1 file changed, 343 insertions(+), 382 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index cf2ac8e5c..d8a8d128c 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -12,8 +12,10 @@ import time import sys import select import webbrowser +import urllib.parse from pathlib import Path from random import randint +from utils import removeHtml from utils import getStatusNumber from utils import loadJson from utils import saveJson @@ -22,7 +24,6 @@ from utils import getDomainFromActor from utils import getFullDomain from utils import isPGPEncrypted from session import createSession -from speaker import getSpeakerFromServer from speaker import getSpeakerPitch from speaker import getSpeakerRate from speaker import getSpeakerRange @@ -31,6 +32,7 @@ from like import sendUndoLikeViaServer from follow import sendFollowRequestViaServer from follow import sendUnfollowRequestViaServer from posts import sendPostViaServer +from posts import c2sBoxJson from announce import sendAnnounceViaServer from pgp import pgpDecrypt from pgp import hasLocalPGPkey @@ -391,62 +393,89 @@ def _safeMessage(content: str) -> str: return content.replace('`', '').replace('$(', '$ (') -def _readLocalBoxPost(boxName: str, index: int, +def _timelineIsEmpty(boxJson: {}) -> bool: + """Returns true if the given timeline is empty + """ + empty = False + if not boxJson: + empty = True + else: + if not isinstance(boxJson, dict): + empty = True + elif not boxJson.get('orderedItems'): + empty = True + return empty + + +def _getFirstItemId(boxJson: {}) -> str: + """Returns the id of the first item in the timeline + """ + if _timelineIsEmpty(boxJson): + return + if len(boxJson['orderedItems']) == 0: + return + return boxJson['orderedItems'][0]['id'] + + +def _textOnlyContent(content: str) -> str: + """Remove formatting from the given string + """ + content = urllib.parse.unquote_plus(content) + content = html.unescape(content) + return removeHtml(content) + + +def _readLocalBoxPost(boxName: str, + pageNumber: int, index: int, boxJson: {}, systemLanguage: str, screenreader: str, espeak) -> {}: """Reads a post from the given timeline Returns the speaker json """ - speakerJson = _getSpeakerJsonFromIndex(boxName, index) - if not speakerJson: + if _timelineIsEmpty(boxJson): return - nameStr = speakerJson['name'] + postJsonObject = _desktopGetBoxPostObject(boxJson, index) + if not postJsonObject: + return + actor = postJsonObject['object']['attributedTo'] + nameStr = getNicknameFromActor(actor) gender = 'They/Them' - if speakerJson.get('gender'): - gender = speakerJson['gender'] - # append image description if needed - if not speakerJson.get('imageDescription'): - messageStr = speakerJson['say'] - else: - messageStr = speakerJson['say'] + '. ' + \ - speakerJson['imageDescription'] - - content = messageStr - if speakerJson.get('content'): - content = speakerJson['content'] - - sayStr = 'Reading ' + boxName + ' post ' + str(index) + '.' + content = _textOnlyContent(postJsonObject['object']['content']) + sayStr = 'Reading ' + boxName + ' post ' + str(index) + \ + ' from page ' + str(pageNumber) + '.' sayStr2 = sayStr.replace(' dm ', ' DM ') _sayCommand(sayStr, sayStr2, screenreader, systemLanguage, espeak) - if speakerJson.get('id') and isPGPEncrypted(content): + if isPGPEncrypted(content): sayStr = 'Encrypted message. Please enter your passphrase.' _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) - content = pgpDecrypt(content, speakerJson['id']) + content = pgpDecrypt(content, actor) if isPGPEncrypted(content): sayStr = 'Message could not be decrypted' _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) return content = _safeMessage(content) - messageStr = _safeMessage(messageStr) + messageStr = content - time.sleep(2) + if screenreader: + time.sleep(2) # say the speaker's name _sayCommand(nameStr, nameStr, screenreader, systemLanguage, espeak, nameStr, gender) - time.sleep(2) + if screenreader: + time.sleep(2) # speak the post content _sayCommand(content, messageStr, screenreader, systemLanguage, espeak, nameStr, gender) - return speakerJson + return postJsonObject def _desktopShowBox(notifyJson: {}, boxName: str, @@ -591,6 +620,114 @@ def _desktopShowBox(notifyJson: {}, boxName: str, return True +def _desktopGetBoxPostObject(boxJson: {}, index: int) -> {}: + """Gets the post with the given index from the timeline + """ + ctr = 0 + for postJsonObject in boxJson['orderedItems']: + if not postJsonObject.get('object'): + continue + if not isinstance(postJsonObject['object'], dict): + continue + if not postJsonObject['object'].get('published'): + continue + if not postJsonObject['object'].get('content'): + continue + ctr += 1 + if ctr == index: + return postJsonObject + return None + + +def _desktopShowBoxJson(boxName: str, boxJson: {}, + screenreader: str, systemLanguage: str, espeak, + pageNumber=1, + newReplies=False, + newDMs=False) -> bool: + """Shows online timeline + """ + indent = ' ' + + # title + _desktopClearScreen() + _desktopShowBanner() + + notificationIcons = '' + titleStr = '\33[7m' + boxName.upper() + '\33[0m' + # titleStr += ' page ' + str(pageNumber) + if notificationIcons: + while len(titleStr) < 95 - len(notificationIcons): + titleStr += ' ' + titleStr += notificationIcons + print(indent + titleStr + '\n') + + if _timelineIsEmpty(boxJson): + boxStr = boxName + if boxName == 'dm': + boxStr = 'DM' + sayStr = indent + 'You have no ' + boxStr + ' posts yet.' + _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) + print('') + return False + + ctr = 1 + for postJsonObject in boxJson['orderedItems']: + if not postJsonObject.get('object'): + continue + if not isinstance(postJsonObject['object'], dict): + continue + if not postJsonObject['object'].get('published'): + continue + if not postJsonObject['object'].get('content'): + continue + published = postJsonObject['published'].replace('T', ' ') + posStr = str(ctr) + '.' + while len(posStr) < 3: + posStr += ' ' + authorActor = postJsonObject['object']['attributedTo'] + name = getNicknameFromActor(authorActor) + if len(name) > 16: + name = name[:16] + else: + while len(name) < 16: + name += ' ' + content = _textOnlyContent(postJsonObject['object']['content']) + if isPGPEncrypted(content): + content = '🔒' + content + elif '://' in content: + content = '🔗' + content + if len(content) > 40: + content = content[:40] + else: + while len(content) < 40: + content += ' ' + print(indent + str(posStr) + ' | ' + name + ' | ' + + published + ' | ' + content) + ctr += 1 + + print('') + + # say the post number range + sayStr = indent + boxName + ' page ' + str(pageNumber) + \ + ' containing ' + str(ctr - 1) + ' posts. ' + if newDMs and boxName != 'dm': + sayStr += \ + 'Use \33[3mshow dm\33[0m to view direct messages.' + elif newReplies and boxName != 'replies': + sayStr += \ + 'Use \33[3mshow replies\33[0m to view reply posts.' + else: + sayStr += \ + 'Use the \33[3mnext\33[0m and ' + \ + '\33[3mprev\33[0m commands to navigate.' + sayStr2 = sayStr.replace('\33[3m', '').replace('\33[0m', '') + sayStr2 = sayStr2.replace('show dm', 'show DM') + sayStr2 = sayStr2.replace('dm post', 'Direct message post') + _sayCommand(sayStr, sayStr2, screenreader, systemLanguage, espeak) + print('') + return True + + def _desktopNewDM(session, toHandle: str, baseDir: str, nickname: str, password: str, domain: str, port: int, httpPrefix: str, @@ -811,50 +948,41 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) - currTimeline = '' - currInboxIndex = 0 - if not showNewPosts: - print('') - currInboxIndex = 0 - _desktopShowBox(None, 'inbox', - screenreader, systemLanguage, espeak, - currInboxIndex, 10) - currTimeline = 'inbox' - print('') - commandStr = _desktopWaitForCmd(2, debug) - nextCommandStr = None + currTimeline = 'inbox' + pageNumber = 1 + postJsonObject = {} originalScreenReader = screenreader - domainFull = getFullDomain(domain, port) - actor = httpPrefix + '://' + domainFull + '/users/' + nickname - prevSay = '' - prevCalendar = False - prevFollow = False - prevLike = '' - prevShare = False - dmSoundFilename = 'dm.ogg' - replySoundFilename = 'reply.ogg' - calendarSoundFilename = 'calendar.ogg' - followSoundFilename = 'follow.ogg' - likeSoundFilename = 'like.ogg' - shareSoundFilename = 'share.ogg' - player = 'ffplay' + # domainFull = getFullDomain(domain, port) + # actor = httpPrefix + '://' + domainFull + '/users/' + nickname + # prevSay = '' + # prevCalendar = False + # prevFollow = False + # prevLike = '' + # prevShare = False + # dmSoundFilename = 'dm.ogg' + # replySoundFilename = 'reply.ogg' + # calendarSoundFilename = 'calendar.ogg' + # followSoundFilename = 'follow.ogg' + # likeSoundFilename = 'like.ogg' + # shareSoundFilename = 'share.ogg' + # player = 'ffplay' nameStr = None gender = None messageStr = None content = None cachedWebfingers = {} personCache = {} - currDMIndex = 0 - currRepliesIndex = 0 - currSentIndex = 0 newRepliesExist = False newDMsExist = False - currPostId = '' pgpKeyUpload = False - while (1): - session = createSession(proxyType) + sayStr = indent + 'Connecting...' + _sayCommand(sayStr, sayStr, screenreader, + systemLanguage, espeak) + session = createSession(proxyType) + prevTimelineFirstId = '' + while (1): if not pgpKeyUpload: pgpKey = \ pgpPublicKeyUpload(baseDir, session, @@ -866,166 +994,28 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, print('PGP public key uploaded') pgpKeyUpload = True - notifyJson = None - speakerJson = \ - getSpeakerFromServer(baseDir, session, nickname, password, - domain, port, httpPrefix, True, __version__) - if speakerJson: - if speakerJson.get('notify') and speakerJson.get('id'): - notifyJson = speakerJson['notify'] - title = 'Epicyon' - if speakerJson['notify'].get('title'): - title = speakerJson['notify']['title'] - soundsDir = 'theme/default/sounds' - if speakerJson['notify'].get('theme'): - if isinstance(speakerJson['notify']['theme'], str): - soundsDir = \ - 'theme/' + \ - speakerJson['notify']['theme'] + '/sounds' - if not os.path.isdir(soundsDir): - soundsDir = 'theme/default/sounds' + boxJson = c2sBoxJson(baseDir, session, + nickname, password, + domain, port, httpPrefix, + currTimeline, pageNumber, + debug) - indicatorDM = False - if speakerJson.get('direct'): - if speakerJson['direct'] is True: - indicatorDM = True - indicatorReplies = False - if speakerJson.get('replyToYou'): - if speakerJson['replyToYou'] is True: - indicatorReplies = True - - if indicatorDM: - if currPostId != speakerJson['id']: - if notificationSounds: - _playNotificationSound(soundsDir + '/' + - dmSoundFilename, player) - _desktopNotification(notificationType, title, - 'New direct message ' + - actor + '/dm') - elif indicatorReplies: - if currPostId != speakerJson['id']: - if notificationSounds: - _playNotificationSound(soundsDir + '/' + - replySoundFilename, - player) - _desktopNotification(notificationType, title, - 'New reply ' + - actor + '/tlreplies') - elif speakerJson['notify']['calendar'] != prevCalendar: - if speakerJson['notify']['calendar'] is True: - if notificationSounds: - _playNotificationSound(soundsDir + '/' + - calendarSoundFilename, - player) - _desktopNotification(notificationType, title, - 'New calendar event ' + - actor + '/calendar') - prevCalendar = speakerJson['notify']['calendar'] - elif speakerJson['notify']['followRequests'] != prevFollow: - if speakerJson['notify']['followRequests'] is True: - if notificationSounds: - _playNotificationSound(soundsDir + '/' + - followSoundFilename, - player) - _desktopNotification(notificationType, title, - 'New follow request ' + - actor + '/followers#buttonheader') - prevFollow = speakerJson['notify']['followRequests'] - elif speakerJson['notify']['likedBy'] != prevLike: - if '##sent##' not in speakerJson['notify']['likedBy']: - if notificationSounds: - _playNotificationSound(soundsDir + '/' + - likeSoundFilename, player) - _desktopNotification(notificationType, title, - 'New like ' + - speakerJson['notify']['likedBy']) - prevLike = speakerJson['notify']['likedBy'] - elif speakerJson['notify']['share'] != prevShare: - if speakerJson['notify']['share'] is True: - if notificationSounds: - _playNotificationSound(soundsDir + '/' + - shareSoundFilename, - player) - _desktopNotification(notificationType, title, - 'New shared item ' + - actor + '/shares') - prevShare = speakerJson['notify']['share'] - - if speakerJson.get('say'): - if speakerJson['say'] != prevSay: - if speakerJson.get('name'): - nameStr = speakerJson['name'] - gender = 'They/Them' - if speakerJson.get('gender'): - gender = speakerJson['gender'] - - # append image description if needed - if not speakerJson.get('imageDescription'): - messageStr = speakerJson['say'] - else: - messageStr = speakerJson['say'] + '. ' + \ - speakerJson['imageDescription'] - encryptedMessage = False - if speakerJson.get('id') and \ - isPGPEncrypted(messageStr): - encryptedMessage = True - - content = messageStr - if speakerJson.get('content'): - if not encryptedMessage: - content = speakerJson['content'] - else: - content = '🔒 Encrypted message' - - if showNewPosts: - # say the speaker's name - _sayCommand(nameStr, nameStr, screenreader, - systemLanguage, espeak, - nameStr, gender) - - time.sleep(2) - - # speak the post content - content = _safeMessage(content) - messageStr = _safeMessage(messageStr) - _sayCommand(content, messageStr, screenreader, - systemLanguage, espeak, - nameStr, gender) - - # store incoming post - speakerJson['decrypted'] = False - if speakerJson.get('replyToYou'): - newRepliesExist = True - _desktopStoreMsg(speakerJson, 'replies') - if speakerJson.get('direct'): - newDMsExist = True - _desktopStoreMsg(speakerJson, 'dm') - if storeInboxPosts: - _desktopStoreMsg(speakerJson, 'inbox') - - if not showNewPosts: - _desktopClearScreen() - _desktopShowBox(notifyJson, currTimeline, - None, systemLanguage, espeak, - currInboxIndex, 10, - newRepliesExist, - newDMsExist) - else: - print('') - - prevSay = speakerJson['say'] - if speakerJson.get('id'): - currPostId = speakerJson['id'] + if boxJson: + timelineFirstId = _getFirstItemId(boxJson) + if timelineFirstId != prevTimelineFirstId: + _desktopClearScreen() + _desktopShowBoxJson(currTimeline, boxJson, + None, systemLanguage, espeak, + pageNumber, + newRepliesExist, + newDMsExist) + prevTimelineFirstId = timelineFirstId # wait for a while, or until a key is pressed if noKeyPress: time.sleep(10) else: - if nextCommandStr: - commandStr = nextCommandStr - nextCommandStr = None - else: - commandStr = _desktopWaitForCmd(30, debug) + commandStr = _desktopWaitForCmd(30, debug) if commandStr: if commandStr.startswith('/'): commandStr = commandStr[1:] @@ -1039,135 +1029,121 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, commandStr = _desktopWaitForCmd(2, debug) break elif commandStr.startswith('show dm'): - currDMIndex = 0 - _desktopShowBox(notifyJson, 'dm', - screenreader, systemLanguage, espeak, - currDMIndex, 10, - newRepliesExist, newDMsExist) + pageNumber = 1 + prevTimelineFirstId = '' currTimeline = 'dm' + boxJson = c2sBoxJson(baseDir, session, + nickname, password, + domain, port, httpPrefix, + currTimeline, pageNumber, + debug) + if boxJson: + _desktopShowBoxJson(currTimeline, boxJson, + screenreader, systemLanguage, espeak, + pageNumber, + newRepliesExist, newDMsExist) newDMsExist = False elif commandStr.startswith('show rep'): - currRepliesIndex = 0 - _desktopShowBox(notifyJson, 'replies', - screenreader, systemLanguage, espeak, - currRepliesIndex, 10, - newRepliesExist, newDMsExist) + pageNumber = 1 + prevTimelineFirstId = '' currTimeline = 'replies' + boxJson = c2sBoxJson(baseDir, session, + nickname, password, + domain, port, httpPrefix, + currTimeline, pageNumber, + debug) + if boxJson: + _desktopShowBoxJson(currTimeline, boxJson, + screenreader, systemLanguage, espeak, + pageNumber, + newRepliesExist, newDMsExist) # Turn off the replies indicator newRepliesExist = False elif commandStr.startswith('show sen'): - currSentIndex = 0 - _desktopShowBox(notifyJson, 'sent', - screenreader, systemLanguage, espeak, - currSentIndex, 10, - newRepliesExist, newDMsExist) - currTimeline = 'sent' + pageNumber = 1 + prevTimelineFirstId = '' + currTimeline = 'outbox' + boxJson = c2sBoxJson(baseDir, session, + nickname, password, + domain, port, httpPrefix, + currTimeline, pageNumber, + debug) + if boxJson: + _desktopShowBoxJson(currTimeline, boxJson, + screenreader, systemLanguage, espeak, + pageNumber, + newRepliesExist, newDMsExist) elif (commandStr == 'show' or commandStr.startswith('show in') or commandStr == 'clear'): - currInboxIndex = 0 - _desktopShowBox(notifyJson, 'inbox', - screenreader, systemLanguage, espeak, - currInboxIndex, 10, - newRepliesExist, newDMsExist) + pageNumber = 1 + prevTimelineFirstId = '' currTimeline = 'inbox' + boxJson = c2sBoxJson(baseDir, session, + nickname, password, + domain, port, httpPrefix, + currTimeline, pageNumber, + debug) + if boxJson: + _desktopShowBoxJson(currTimeline, boxJson, + screenreader, systemLanguage, espeak, + pageNumber, + newRepliesExist, newDMsExist) elif commandStr.startswith('next'): - if currTimeline == 'dm': - currDMIndex += 10 - _desktopShowBox(notifyJson, 'dm', - screenreader, systemLanguage, espeak, - currDMIndex, 10, - newRepliesExist, newDMsExist) - elif currTimeline == 'replies': - currRepliesIndex += 10 - _desktopShowBox(notifyJson, 'replies', - screenreader, systemLanguage, espeak, - currRepliesIndex, 10, - newRepliesExist, newDMsExist) - elif currTimeline == 'sent': - currSentIndex += 10 - _desktopShowBox(notifyJson, 'sent', - screenreader, systemLanguage, espeak, - currSentIndex, 10, - newRepliesExist, newDMsExist) - elif currTimeline == 'inbox': - currInboxIndex += 10 - _desktopShowBox(notifyJson, 'inbox', - screenreader, systemLanguage, espeak, - currInboxIndex, 10, - newRepliesExist, newDMsExist) + pageNumber += 1 + prevTimelineFirstId = '' + boxJson = c2sBoxJson(baseDir, session, + nickname, password, + domain, port, httpPrefix, + currTimeline, pageNumber, + debug) + if boxJson: + _desktopShowBoxJson(currTimeline, boxJson, + screenreader, systemLanguage, espeak, + pageNumber, + newRepliesExist, newDMsExist) elif commandStr.startswith('prev'): - if currTimeline == 'dm': - currDMIndex -= 10 - if currDMIndex < 0: - currDMIndex = 0 - _desktopShowBox(notifyJson, 'dm', - screenreader, systemLanguage, espeak, - currDMIndex, 10, - newRepliesExist, newDMsExist) - elif currTimeline == 'replies': - currRepliesIndex -= 10 - if currRepliesIndex < 0: - currRepliesIndex = 0 - _desktopShowBox(notifyJson, 'replies', - screenreader, systemLanguage, espeak, - currRepliesIndex, 10, - newRepliesExist, newDMsExist) - elif currTimeline == 'sent': - currSentIndex -= 10 - if currSentIndex < 0: - currSentIndex = 0 - _desktopShowBox(notifyJson, 'sent', - screenreader, systemLanguage, espeak, - currSentIndex, 10, - newRepliesExist, newDMsExist) - elif currTimeline == 'inbox': - currInboxIndex -= 10 - if currInboxIndex < 0: - currInboxIndex = 0 - _desktopShowBox(notifyJson, 'inbox', - screenreader, systemLanguage, espeak, - currInboxIndex, 10, - newRepliesExist, newDMsExist) + pageNumber -= 1 + if pageNumber < 1: + pageNumber = 1 + prevTimelineFirstId = '' + boxJson = c2sBoxJson(baseDir, session, + nickname, password, + domain, port, httpPrefix, + currTimeline, pageNumber, + debug) + if boxJson: + _desktopShowBoxJson(currTimeline, boxJson, + screenreader, systemLanguage, espeak, + pageNumber, + newRepliesExist, newDMsExist) elif commandStr.startswith('read ') or commandStr == 'read': if commandStr == 'read': postIndexStr = '1' else: postIndexStr = commandStr.split('read ')[1] - if postIndexStr.isdigit(): + if boxJson and postIndexStr.isdigit(): postIndex = int(postIndexStr) - speakerJson = \ - _readLocalBoxPost(currTimeline, postIndex, + postJsonObject = \ + _readLocalBoxPost(currTimeline, + pageNumber, postIndex, boxJson, systemLanguage, screenreader, espeak) - # if we are on a busy timeline then wait for the post - # to be read because otherwise it could potentially be - # immediately overwritten as the timeline refreshes - if speakerJson and not noKeyPress: - # average reading speed is said to be 800 chars/min - # so this allows some overhead - readingSpeedCharsPerMin = 600 - displayTimeSec = \ - int(len(speakerJson['say']) * 60 / - readingSpeedCharsPerMin) - if displayTimeSec < 10: - displayTimeSec = 10 - nextCommandStr = \ - _desktopWaitForCmd(displayTimeSec, debug) print('') elif commandStr == 'reply' or commandStr == 'r': - if speakerJson.get('id'): - postId = speakerJson['id'] - subject = None - if speakerJson.get('summary'): - subject = speakerJson['summary'] - sessionReply = createSession(proxyType) - _desktopReplyToPost(sessionReply, postId, - baseDir, nickname, password, - domain, port, httpPrefix, - cachedWebfingers, personCache, - debug, subject, - screenreader, systemLanguage, - espeak) + if postJsonObject: + if postJsonObject.get('id'): + postId = postJsonObject['id'] + subject = None + if postJsonObject['object'].get('summary'): + subject = postJsonObject['object']['summary'] + sessionReply = createSession(proxyType) + _desktopReplyToPost(sessionReply, postId, + baseDir, nickname, password, + domain, port, httpPrefix, + cachedWebfingers, personCache, + debug, subject, + screenreader, systemLanguage, + espeak) print('') elif (commandStr == 'post' or commandStr == 'p' or commandStr == 'send' or @@ -1212,50 +1188,61 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, espeak) print('') elif commandStr == 'like': - if speakerJson.get('id'): - sayStr = 'Liking post by ' + speakerJson['name'] - _sayCommand(sayStr, sayStr, - screenreader, - systemLanguage, espeak) - sessionLike = createSession(proxyType) - sendLikeViaServer(baseDir, sessionLike, - nickname, password, - domain, port, - httpPrefix, speakerJson['id'], - cachedWebfingers, personCache, - False, __version__) - print('') - elif commandStr == 'unlike' or commandStr == 'undo like': - if speakerJson.get('id'): - sayStr = 'Undoing like of post by ' + speakerJson['name'] - _sayCommand(sayStr, sayStr, - screenreader, - systemLanguage, espeak) - sessionUnlike = createSession(proxyType) - sendUndoLikeViaServer(baseDir, sessionUnlike, + if postJsonObject: + if postJsonObject.get('id'): + likeActor = postJsonObject['object']['attributedTo'] + sayStr = 'Liking post by ' + \ + getNicknameFromActor(likeActor) + _sayCommand(sayStr, sayStr, + screenreader, + systemLanguage, espeak) + sessionLike = createSession(proxyType) + sendLikeViaServer(baseDir, sessionLike, nickname, password, - domain, port, - httpPrefix, speakerJson['id'], + domain, port, httpPrefix, + postJsonObject['id'], cachedWebfingers, personCache, False, __version__) - print('') + print('') + elif commandStr == 'unlike' or commandStr == 'undo like': + if postJsonObject: + if postJsonObject.get('id'): + unlikeActor = postJsonObject['object']['attributedTo'] + sayStr = \ + 'Undoing like of post by ' + \ + getNicknameFromActor(unlikeActor) + _sayCommand(sayStr, sayStr, + screenreader, + systemLanguage, espeak) + sessionUnlike = createSession(proxyType) + sendUndoLikeViaServer(baseDir, sessionUnlike, + nickname, password, + domain, port, httpPrefix, + postJsonObject['id'], + cachedWebfingers, personCache, + False, __version__) + print('') elif (commandStr == 'announce' or commandStr == 'boost' or commandStr == 'retweet'): - if speakerJson.get('id'): - postId = speakerJson['id'] - sayStr = 'Announcing post by ' + speakerJson['name'] - _sayCommand(sayStr, sayStr, - screenreader, - systemLanguage, espeak) - sessionAnnounce = createSession(proxyType) - sendAnnounceViaServer(baseDir, sessionAnnounce, - nickname, password, - domain, port, - httpPrefix, postId, - cachedWebfingers, personCache, - True, __version__) - print('') + if postJsonObject: + if postJsonObject.get('id'): + postId = postJsonObject['id'] + announceActor = \ + postJsonObject['object']['attributedTo'] + sayStr = 'Announcing post by ' + \ + getNicknameFromActor(announceActor) + _sayCommand(sayStr, sayStr, + screenreader, + systemLanguage, espeak) + sessionAnnounce = createSession(proxyType) + sendAnnounceViaServer(baseDir, sessionAnnounce, + nickname, password, + domain, port, + httpPrefix, postId, + cachedWebfingers, personCache, + True, __version__) + print('') elif commandStr.startswith('follow '): followHandle = commandStr.replace('follow ', '').strip() if followHandle.startswith('@'): @@ -1394,31 +1381,5 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, _sayCommand(sayStr, sayStr, originalScreenReader, systemLanguage, espeak) print('') - elif commandStr.startswith('accept'): - if notifyJson: - if notifyJson.get('followRequestsList'): - if len(notifyJson['followRequestsList']) > 0: - sayStr = 'Accepting follow request for ' + \ - notifyJson['followRequestsList'][0] - _sayCommand(sayStr, sayStr, originalScreenReader, - systemLanguage, espeak) - # TODO - sayStr = 'This command is not yet implemented' - _sayCommand(sayStr, sayStr, originalScreenReader, - systemLanguage, espeak) - print('') - elif commandStr.startswith('reject'): - if notifyJson: - if notifyJson.get('followRequestsList'): - if len(notifyJson['followRequestsList']) > 0: - sayStr = 'Rejecting follow request for ' + \ - notifyJson['followRequestsList'][0] - _sayCommand(sayStr, sayStr, originalScreenReader, - systemLanguage, espeak) - # TODO - sayStr = 'This command is not yet implemented' - _sayCommand(sayStr, sayStr, originalScreenReader, - systemLanguage, espeak) - print('') elif commandStr.startswith('h'): _desktopHelp() From c9e36e857741d79720bf02b8ecba8c3a2bba07af Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 18 Mar 2021 17:27:46 +0000 Subject: [PATCH 23/87] Add speakable text for desktop client --- daemon.py | 31 +++++-------------------------- desktop_client.py | 20 +++++++++++++++----- epicyon.py | 1 + speaker.py | 24 ++++++++++++++++++++++++ utils.py | 29 +++++++++++++++++++++++++++++ 5 files changed, 74 insertions(+), 31 deletions(-) diff --git a/daemon.py b/daemon.py index c7edcc342..219f2ed28 100644 --- a/daemon.py +++ b/daemon.py @@ -10,7 +10,6 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer, HTTPServer import sys import json import time -import locale import urllib.parse import datetime from socket import error as SocketError @@ -192,6 +191,7 @@ from shares import addShare from shares import removeShare from shares import expireShares from categories import setHashtagCategory +from utils import loadTranslationsFromFile from utils import getLocalNetworkAddresses from utils import decodedHost from utils import isPublicPost @@ -14443,32 +14443,11 @@ def runDaemon(brochMode: bool, httpd.translate = {} httpd.systemLanguage = 'en' if not unitTest: - if not os.path.isdir(baseDir + '/translations'): - print('ERROR: translations directory not found') - return - if not language: - systemLanguage = locale.getdefaultlocale()[0] - else: - systemLanguage = language - if not systemLanguage: - systemLanguage = 'en' - if '_' in systemLanguage: - systemLanguage = systemLanguage.split('_')[0] - while '/' in systemLanguage: - systemLanguage = systemLanguage.split('/')[1] - if '.' in systemLanguage: - systemLanguage = systemLanguage.split('.')[0] - translationsFile = baseDir + '/translations/' + \ - systemLanguage + '.json' - if not os.path.isfile(translationsFile): - systemLanguage = 'en' - translationsFile = baseDir + '/translations/' + \ - systemLanguage + '.json' - print('System language: ' + systemLanguage) - httpd.systemLanguage = systemLanguage - httpd.translate = loadJson(translationsFile) + httpd.translate, httpd.systemLanguage = \ + loadTranslationsFromFile(baseDir, language) + print('System language: ' + httpd.systemLanguage) if not httpd.translate: - print('ERROR: no translations loaded from ' + translationsFile) + print('ERROR: no translations were loaded') sys.exit() # For moderated newswire feeds this is the amount of time allowed diff --git a/desktop_client.py b/desktop_client.py index d8a8d128c..c1c3eac7e 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -15,6 +15,7 @@ import webbrowser import urllib.parse from pathlib import Path from random import randint +from utils import loadTranslationsFromFile from utils import removeHtml from utils import getStatusNumber from utils import loadJson @@ -24,6 +25,7 @@ from utils import getDomainFromActor from utils import getFullDomain from utils import isPGPEncrypted from session import createSession +from speaker import speakableText from speaker import getSpeakerPitch from speaker import getSpeakerRate from speaker import getSpeakerRange @@ -425,10 +427,11 @@ def _textOnlyContent(content: str) -> str: return removeHtml(content) -def _readLocalBoxPost(boxName: str, +def _readLocalBoxPost(baseDir: str, boxName: str, pageNumber: int, index: int, boxJson: {}, systemLanguage: str, - screenreader: str, espeak) -> {}: + screenreader: str, espeak, + translate: {}) -> {}: """Reads a post from the given timeline Returns the speaker json """ @@ -458,7 +461,7 @@ def _readLocalBoxPost(boxName: str, return content = _safeMessage(content) - messageStr = content + messageStr = speakableText(baseDir, content, translate) if screenreader: time.sleep(2) @@ -911,6 +914,7 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, noKeyPress: bool, storeInboxPosts: bool, showNewPosts: bool, + language: str, debug: bool) -> None: """Runs the desktop and screen reader client, which announces new inbox items @@ -977,6 +981,12 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, newDMsExist = False pgpKeyUpload = False + sayStr = indent + 'Loading translations file...' + _sayCommand(sayStr, sayStr, screenreader, + systemLanguage, espeak) + translate, systemLanguage = \ + loadTranslationsFromFile(baseDir, language) + sayStr = indent + 'Connecting...' _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) @@ -1124,10 +1134,10 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, if boxJson and postIndexStr.isdigit(): postIndex = int(postIndexStr) postJsonObject = \ - _readLocalBoxPost(currTimeline, + _readLocalBoxPost(baseDir, currTimeline, pageNumber, postIndex, boxJson, systemLanguage, screenreader, - espeak) + espeak, translate) print('') elif commandStr == 'reply' or commandStr == 'r': if postJsonObject: diff --git a/epicyon.py b/epicyon.py index 3052c1f06..4e3971f24 100644 --- a/epicyon.py +++ b/epicyon.py @@ -1926,6 +1926,7 @@ if args.desktop: args.noKeyPress, storeInboxPosts, args.notifyShowNewPosts, + args.language, args.debug) sys.exit() diff --git a/speaker.py b/speaker.py index 2ec228b95..f2561fd87 100644 --- a/speaker.py +++ b/speaker.py @@ -405,6 +405,30 @@ def getSSMLbox(baseDir: str, path: str, instanceTitle, gender) +def speakableText(baseDir: str, content: str, translate: {}) -> str: + """Convert the given text to a speakable version + which includes changes for prononciation + """ + if isPGPEncrypted(content): + return + + # replace some emoji before removing html + if ' <3' in content: + content = content.replace(' <3', ' ' + translate['heart']) + content = removeHtml(htmlReplaceQuoteMarks(content)) + detectedLinks = [] + content = speakerReplaceLinks(content, translate, detectedLinks) + # replace all double spaces + while ' ' in content: + content = content.replace(' ', ' ') + content = content.replace(' . ', '. ').strip() + sayContent = _speakerPronounce(baseDir, content, translate) + # replace all double spaces + while ' ' in sayContent: + sayContent = sayContent.replace(' ', ' ') + return sayContent.replace(' . ', '. ').strip() + + def _postToSpeakerJson(baseDir: str, httpPrefix: str, nickname: str, domain: str, domainFull: str, postJsonObject: {}, personCache: {}, diff --git a/utils.py b/utils.py index 35d42a4e8..35dc00072 100644 --- a/utils.py +++ b/utils.py @@ -13,6 +13,7 @@ import shutil import datetime import json import idna +import locale from pprint import pprint from calendar import monthrange from followingCalendar import addPersonToCalendar @@ -2150,3 +2151,31 @@ def isPGPEncrypted(content: str) -> bool: if '--END PGP MESSAGE--' in content: return True return False + + +def loadTranslationsFromFile(baseDir: str, language: str) -> ({}, str): + """Returns the translations dictionary + """ + if not os.path.isdir(baseDir + '/translations'): + print('ERROR: translations directory not found') + return + if not language: + systemLanguage = locale.getdefaultlocale()[0] + else: + systemLanguage = language + if not systemLanguage: + systemLanguage = 'en' + if '_' in systemLanguage: + systemLanguage = systemLanguage.split('_')[0] + while '/' in systemLanguage: + systemLanguage = systemLanguage.split('/')[1] + if '.' in systemLanguage: + systemLanguage = systemLanguage.split('.')[0] + translationsFile = baseDir + '/translations/' + \ + systemLanguage + '.json' + if not os.path.isfile(translationsFile): + systemLanguage = 'en' + translationsFile = baseDir + '/translations/' + \ + systemLanguage + '.json' + print('System language: ' + systemLanguage) + return loadJson(translationsFile), systemLanguage From dbb2c3963201fc8a2240e9b4e216b7bfe6ece514 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 18 Mar 2021 17:30:47 +0000 Subject: [PATCH 24/87] Sequence --- desktop_client.py | 9 +++++---- utils.py | 1 - 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index c1c3eac7e..1a36d2179 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -948,9 +948,6 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, sayStr = indent + 'Notification sounds off' _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) - sayStr = indent + '/q or /quit to exit' - _sayCommand(sayStr, sayStr, screenreader, - systemLanguage, espeak) currTimeline = 'inbox' pageNumber = 1 @@ -981,7 +978,7 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, newDMsExist = False pgpKeyUpload = False - sayStr = indent + 'Loading translations file...' + sayStr = indent + 'Loading translations file' _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) translate, systemLanguage = \ @@ -991,6 +988,10 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) session = createSession(proxyType) + + sayStr = indent + '/q or /quit to exit' + _sayCommand(sayStr, sayStr, screenreader, + systemLanguage, espeak) prevTimelineFirstId = '' while (1): if not pgpKeyUpload: diff --git a/utils.py b/utils.py index 35dc00072..0090abd7e 100644 --- a/utils.py +++ b/utils.py @@ -2177,5 +2177,4 @@ def loadTranslationsFromFile(baseDir: str, language: str) -> ({}, str): systemLanguage = 'en' translationsFile = baseDir + '/translations/' + \ systemLanguage + '.json' - print('System language: ' + systemLanguage) return loadJson(translationsFile), systemLanguage From 62908009e1a50c2108f94f843e92cba0e5c61909 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 18 Mar 2021 17:37:14 +0000 Subject: [PATCH 25/87] Announce pgp key upload --- desktop_client.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index 1a36d2179..e62b3193d 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -995,14 +995,18 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, prevTimelineFirstId = '' while (1): if not pgpKeyUpload: + sayStr = indent + 'Uploading PGP public key' + _sayCommand(sayStr, sayStr, screenreader, + systemLanguage, espeak) pgpKey = \ pgpPublicKeyUpload(baseDir, session, nickname, password, domain, port, httpPrefix, cachedWebfingers, personCache, debug, False) - if pgpKey: - print('PGP public key uploaded') + sayStr = indent + 'PGP public key uploaded' + _sayCommand(sayStr, sayStr, screenreader, + systemLanguage, espeak) pgpKeyUpload = True boxJson = c2sBoxJson(baseDir, session, From 5d7b24d7246746f5e7487c678c694185f5fe56c0 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 18 Mar 2021 17:42:24 +0000 Subject: [PATCH 26/87] Tidying --- desktop_client.py | 153 ++-------------------------------------------- 1 file changed, 5 insertions(+), 148 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index e62b3193d..17d264838 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -481,148 +481,6 @@ def _readLocalBoxPost(baseDir: str, boxName: str, return postJsonObject -def _desktopShowBox(notifyJson: {}, boxName: str, - screenreader: str, systemLanguage: str, espeak, - startPostIndex=0, noOfPosts=10, - newReplies=False, - newDMs=False) -> bool: - """Shows locally stored posts for a given subdirectory - """ - indent = ' ' - homeDir = str(Path.home()) - if not os.path.isdir(homeDir + '/.config'): - os.mkdir(homeDir + '/.config') - if not os.path.isdir(homeDir + '/.config/epicyon'): - os.mkdir(homeDir + '/.config/epicyon') - msgDir = homeDir + '/.config/epicyon/' + boxName - if not os.path.isdir(msgDir): - os.mkdir(msgDir) - index = [] - for subdir, dirs, files in os.walk(msgDir): - for f in files: - if not f.endswith('.json'): - continue - index.append(f) - - # title - _desktopClearScreen() - _desktopShowBanner() - - notificationIcons = '' - if notifyJson: - if notifyJson.get('followRequests'): - notificationIcons += ' 👤' - if newDMs: - notificationIcons += ' 📩' - if newReplies: - notificationIcons += ' 📨' - if notifyJson.get('calendar'): - notificationIcons += ' 📅' - if notifyJson.get('share'): - notificationIcons += ' 🤝' - if notifyJson.get('likedBy'): - if '##sent##' not in notifyJson['likedBy']: - notificationIcons += ' ❤' - titleStr = '\33[7m' + boxName.upper() + '\33[0m' - if notificationIcons: - while len(titleStr) < 95 - len(notificationIcons): - titleStr += ' ' - titleStr += notificationIcons - print(indent + titleStr + '\n') - - if not index: - boxStr = boxName - if boxName == 'dm': - boxStr = 'DM' - sayStr = indent + 'You have no ' + boxStr + ' posts yet.' - _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) - print('') - return False - - maxPostIndex = len(index) - index.sort(reverse=True) - ctr = 0 - for pos in range(startPostIndex, startPostIndex + noOfPosts): - if pos >= maxPostIndex: - break - publishedYear = index[pos].split('-')[0] - publishedMonth = index[pos].split('-')[1] - speakerJsonFilename = \ - os.path.join(msgDir, - publishedYear + '/' + - publishedMonth + '/' + index[pos]) - if not os.path.isfile(speakerJsonFilename): - continue - speakerJson = loadJson(speakerJsonFilename) - if not speakerJson.get('published'): - continue - published = speakerJson['published'].replace('T', ' ') - posStr = str(pos + 1) + '.' - while len(posStr) < 3: - posStr += ' ' - if speakerJson.get('name'): - udata = speakerJson['name'] - name = udata.encode("ascii", "ignore").decode().strip() - else: - name = '' - if len(name) > 16: - name = name[:16] - else: - while len(name) < 16: - name += ' ' - content = speakerJson['content'] - if isPGPEncrypted(content): - content = '🔒' + content - elif speakerJson.get('detectedLinks'): - if len(speakerJson['detectedLinks']) > 0: - content = '🔗' + content - if len(content) > 40: - content = content[:40] - else: - while len(content) < 40: - content += ' ' - print(indent + str(posStr) + ' | ' + name + ' | ' + - published + ' | ' + content) - ctr += 1 - - print('') - - # say the post number range - sayStr = indent + boxName + ' posts ' + str(startPostIndex + 1) + \ - ' to ' + str(startPostIndex + ctr) + '. ' - if newDMs and boxName != 'dm': - sayStr += \ - 'Use \33[3mshow dm\33[0m to view direct messages.' - elif newReplies and boxName != 'replies': - sayStr += \ - 'Use \33[3mshow replies\33[0m to view reply posts.' - else: - sayStr += \ - 'Use the \33[3mnext\33[0m and ' + \ - '\33[3mprev\33[0m commands to navigate.' - sayStr2 = sayStr.replace('\33[3m', '').replace('\33[0m', '') - sayStr2 = sayStr2.replace('show dm', 'show DM') - sayStr2 = sayStr2.replace('dm post', 'Direct message post') - _sayCommand(sayStr, sayStr2, screenreader, systemLanguage, espeak) - if notifyJson: - if notifyJson.get('followRequestsList'): - if len(notifyJson['followRequestsList']) > 0: - print('') - sayStr = indent + 'You have a follow request from ' + \ - '\33[7m' + \ - notifyJson['followRequestsList'][0].strip() + '\33[0m' - sayStr2 = sayStr.replace('\33[7m', '').replace('\33[0m', '') - _sayCommand(sayStr, sayStr2, - screenreader, systemLanguage, espeak) - sayStr = indent + 'Use the \33[3maccept\33[0m or ' + \ - '\33[3mreject\33[0m commands to respond.' - sayStr2 = sayStr.replace('\33[3m', '').replace('\33[0m', '') - _sayCommand(sayStr, sayStr2, - screenreader, systemLanguage, espeak) - print('') - return True - - def _desktopGetBoxPostObject(boxJson: {}, index: int) -> {}: """Gets the post with the given index from the timeline """ @@ -998,12 +856,11 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, sayStr = indent + 'Uploading PGP public key' _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) - pgpKey = \ - pgpPublicKeyUpload(baseDir, session, - nickname, password, - domain, port, httpPrefix, - cachedWebfingers, personCache, - debug, False) + pgpPublicKeyUpload(baseDir, session, + nickname, password, + domain, port, httpPrefix, + cachedWebfingers, personCache, + debug, False) sayStr = indent + 'PGP public key uploaded' _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) From 47a6d178e09b44cb2c7814dc2259cc9c41890380 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 18 Mar 2021 18:37:55 +0000 Subject: [PATCH 27/87] Preserve youtube links --- desktop_client.py | 4 ++-- speaker.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index 17d264838..949f03dca 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -542,8 +542,8 @@ def _desktopShowBoxJson(boxName: str, boxJson: {}, if not postJsonObject['object'].get('content'): continue published = postJsonObject['published'].replace('T', ' ') - posStr = str(ctr) + '.' - while len(posStr) < 3: + posStr = str(ctr) + while len(posStr) < 2: posStr += ' ' authorActor = postJsonObject['object']['attributedTo'] name = getNicknameFromActor(authorActor) diff --git a/speaker.py b/speaker.py index f2561fd87..8ce1f082f 100644 --- a/speaker.py +++ b/speaker.py @@ -168,8 +168,10 @@ def speakerReplaceLinks(sayText: str, translate: {}, Instead of reading out potentially very long and meaningless links """ text = sayText + text = text.replace('?v=', '__v=') for ch in speakerRemoveChars: text = text.replace(ch, ' ') + text = text.replace('__v=', '?v=') replacements = {} wordsList = text.split(' ') if translate.get('Linked'): From aa7c78d2f13a6e9c17fdab678a8bfe7315856670 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 18 Mar 2021 19:04:58 +0000 Subject: [PATCH 28/87] Tidying --- desktop_client.py | 128 ++++++++-------------------------------------- speaker.py | 6 +-- 2 files changed, 24 insertions(+), 110 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index 949f03dca..6b7417c95 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -13,16 +13,11 @@ import sys import select import webbrowser import urllib.parse -from pathlib import Path from random import randint from utils import loadTranslationsFromFile from utils import removeHtml -from utils import getStatusNumber -from utils import loadJson -from utils import saveJson from utils import getNicknameFromActor from utils import getDomainFromActor -from utils import getFullDomain from utils import isPGPEncrypted from session import createSession from speaker import speakableText @@ -352,43 +347,6 @@ def _desktopNewPost(session, _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) -def _getSpeakerJsonFromIndex(boxName: str, index: int) -> {}: - """Returns the json for the given post index - """ - homeDir = str(Path.home()) - if not os.path.isdir(homeDir + '/.config'): - os.mkdir(homeDir + '/.config') - if not os.path.isdir(homeDir + '/.config/epicyon'): - os.mkdir(homeDir + '/.config/epicyon') - msgDir = homeDir + '/.config/epicyon/' + boxName - if not os.path.isdir(msgDir): - os.mkdir(msgDir) - indexList = [] - for subdir, dirs, files in os.walk(msgDir): - for f in files: - if not f.endswith('.json'): - continue - indexList.append(f) - indexList.sort(reverse=True) - - index -= 1 - if index <= 0: - index = 0 - if len(indexList) <= index: - return None - - publishedYear = indexList[index].split('-')[0] - publishedMonth = indexList[index].split('-')[1] - speakerJsonFilename = \ - os.path.join(msgDir, - publishedYear + '/' + - publishedMonth + '/' + - indexList[index]) - if not os.path.isfile(speakerJsonFilename): - return None - return loadJson(speakerJsonFilename) - - def _safeMessage(content: str) -> str: """Removes anything potentially unsafe from a string """ @@ -461,7 +419,7 @@ def _readLocalBoxPost(baseDir: str, boxName: str, return content = _safeMessage(content) - messageStr = speakableText(baseDir, content, translate) + messageStr, detectedLinks = speakableText(baseDir, content, translate) if screenreader: time.sleep(2) @@ -619,33 +577,6 @@ def _desktopNewDM(session, toHandle: str, espeak) -def _desktopStoreMsg(speakerJson: {}, boxName: str) -> None: - """Stores a message in your home directory for later reading - which could be offline - """ - if not speakerJson.get('published'): - return - homeDir = str(Path.home()) - if not os.path.isdir(homeDir + '/.config'): - os.mkdir(homeDir + '/.config') - if not os.path.isdir(homeDir + '/.config/epicyon'): - os.mkdir(homeDir + '/.config/epicyon') - msgDir = homeDir + '/.config/epicyon/' + boxName - if not os.path.isdir(msgDir): - os.mkdir(msgDir) - publishedYear = speakerJson['published'].split('-')[0] - yearDir = msgDir + '/' + publishedYear - if not os.path.isdir(yearDir): - os.mkdir(yearDir) - publishedMonth = speakerJson['published'].split('-')[1] - monthDir = yearDir + '/' + publishedMonth - if not os.path.isdir(monthDir): - os.mkdir(monthDir) - - msgFilename = monthDir + '/' + speakerJson['published'] + '.json' - saveJson(speakerJson, msgFilename) - - def _desktopNewDMbase(session, toHandle: str, baseDir: str, nickname: str, password: str, domain: str, port: int, httpPrefix: str, @@ -695,7 +626,6 @@ def _desktopNewDMbase(session, toHandle: str, # if there is a local PGP key then attempt to encrypt the DM # using the PGP public key of the recipient - newMessageOriginal = newMessage if hasLocalPGPkey(): sayStr = \ 'Local PGP key detected...' + \ @@ -740,23 +670,6 @@ def _desktopNewDMbase(session, toHandle: str, attachedImageDescription, cachedWebfingers, personCache, isArticle, debug, None, None, subject) == 0: - # store the DM locally - statusNumber, published = getStatusNumber() - postId = \ - httpPrefix + '://' + getFullDomain(domain, port) + \ - '/users/' + nickname + '/statuses/' + statusNumber - speakerJson = { - "name": nickname, - "summary": "", - "content": newMessageOriginal, - "say": newMessageOriginal, - "published": published, - "imageDescription": "", - "detectedLinks": [], - "id": postId, - "direct": True - } - _desktopStoreMsg(speakerJson, 'sent') sayStr = 'Direct message sent' else: sayStr = 'Direct message failed' @@ -1233,25 +1146,26 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, postIndex = commandStr.split(' ')[-1].strip() if postIndex.isdigit(): currIndex = int(postIndex) - speakerJson = \ - _getSpeakerJsonFromIndex(currTimeline, currIndex) - if not speakerJson: - speakerJson = {} - linkOpened = False - if speakerJson.get('detectedLinks'): - if len(speakerJson['detectedLinks']) > 0: - for url in speakerJson['detectedLinks']: - if '://' in url: - webbrowser.open(url) - linkOpened = True - if linkOpened: - sayStr = 'Opened web links' - _sayCommand(sayStr, sayStr, originalScreenReader, - systemLanguage, espeak) - if not linkOpened: - sayStr = 'There are no web links to open.' - _sayCommand(sayStr, sayStr, originalScreenReader, - systemLanguage, espeak) + if currIndex > 0 and boxJson: + postJsonObject = \ + _desktopGetBoxPostObject(boxJson, currIndex) + if postJsonObject: + content = postJsonObject['object']['content'] + messageStr, detectedLinks = \ + speakableText(baseDir, content, translate) + linkOpened = False + for url in detectedLinks: + if '://' in url: + webbrowser.open(url) + linkOpened = True + if linkOpened: + sayStr = 'Opened web links' + _sayCommand(sayStr, sayStr, originalScreenReader, + systemLanguage, espeak) + else: + sayStr = 'There are no web links to open.' + _sayCommand(sayStr, sayStr, originalScreenReader, + systemLanguage, espeak) print('') elif commandStr.startswith('h'): _desktopHelp() diff --git a/speaker.py b/speaker.py index 8ce1f082f..dd46c7bbd 100644 --- a/speaker.py +++ b/speaker.py @@ -407,12 +407,12 @@ def getSSMLbox(baseDir: str, path: str, instanceTitle, gender) -def speakableText(baseDir: str, content: str, translate: {}) -> str: +def speakableText(baseDir: str, content: str, translate: {}) -> (str, []): """Convert the given text to a speakable version which includes changes for prononciation """ if isPGPEncrypted(content): - return + return content, [] # replace some emoji before removing html if ' <3' in content: @@ -428,7 +428,7 @@ def speakableText(baseDir: str, content: str, translate: {}) -> str: # replace all double spaces while ' ' in sayContent: sayContent = sayContent.replace(' ', ' ') - return sayContent.replace(' . ', '. ').strip() + return sayContent.replace(' . ', '. ').strip(), detectedLinks def _postToSpeakerJson(baseDir: str, httpPrefix: str, From dca5e095baef8c7ad229da0a98a2f1b30bbeda19 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 18 Mar 2021 19:43:10 +0000 Subject: [PATCH 29/87] Icons for dm and reply --- desktop_client.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/desktop_client.py b/desktop_client.py index 6b7417c95..9a504bb8b 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -14,6 +14,7 @@ import select import webbrowser import urllib.parse from random import randint +from utils import isDM from utils import loadTranslationsFromFile from utils import removeHtml from utils import getNicknameFromActor @@ -505,6 +506,10 @@ def _desktopShowBoxJson(boxName: str, boxJson: {}, posStr += ' ' authorActor = postJsonObject['object']['attributedTo'] name = getNicknameFromActor(authorActor) + if isDM(postJsonObject): + name += '📧' + name + if postJsonObject['object'].get('inReplyTo'): + name += '↲' + name if len(name) > 16: name = name[:16] else: From 914d819294d43e7f828950cc1f1772f28caa36d4 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 18 Mar 2021 19:45:31 +0000 Subject: [PATCH 30/87] Equals --- desktop_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index 9a504bb8b..428ca28c4 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -507,9 +507,9 @@ def _desktopShowBoxJson(boxName: str, boxJson: {}, authorActor = postJsonObject['object']['attributedTo'] name = getNicknameFromActor(authorActor) if isDM(postJsonObject): - name += '📧' + name + name = '📧' + name if postJsonObject['object'].get('inReplyTo'): - name += '↲' + name + name = '↲' + name if len(name) > 16: name = name[:16] else: From 4c64119484f1a422ccf5471d9fb740ffc4c43a6c Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 18 Mar 2021 19:49:02 +0000 Subject: [PATCH 31/87] Icon after name --- desktop_client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index 428ca28c4..f8584321b 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -506,10 +506,10 @@ def _desktopShowBoxJson(boxName: str, boxJson: {}, posStr += ' ' authorActor = postJsonObject['object']['attributedTo'] name = getNicknameFromActor(authorActor) - if isDM(postJsonObject): - name = '📧' + name if postJsonObject['object'].get('inReplyTo'): - name = '↲' + name + name += '↲' + if isDM(postJsonObject): + name += '📧' if len(name) > 16: name = name[:16] else: From 5b5354004ec14f62c98ae9610ee74f16d113b11a Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 18 Mar 2021 19:52:17 +0000 Subject: [PATCH 32/87] Don't show dm icons on dm timeline --- desktop_client.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index f8584321b..afb9d38fb 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -507,9 +507,10 @@ def _desktopShowBoxJson(boxName: str, boxJson: {}, authorActor = postJsonObject['object']['attributedTo'] name = getNicknameFromActor(authorActor) if postJsonObject['object'].get('inReplyTo'): - name += '↲' - if isDM(postJsonObject): - name += '📧' + name += ' ↲' + if boxName != 'dm': + if isDM(postJsonObject): + name += '📧' if len(name) > 16: name = name[:16] else: From 83943f80c2a1ec3afef1f41dac0c95a24ab691ca Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 18 Mar 2021 20:04:49 +0000 Subject: [PATCH 33/87] Show likes on timeline --- desktop_client.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/desktop_client.py b/desktop_client.py index afb9d38fb..f4e028b84 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -36,6 +36,7 @@ from pgp import pgpDecrypt from pgp import hasLocalPGPkey from pgp import pgpEncryptToActor from pgp import pgpPublicKeyUpload +from like import noOfLikes def _desktopHelp() -> None: @@ -511,6 +512,11 @@ def _desktopShowBoxJson(boxName: str, boxJson: {}, if boxName != 'dm': if isDM(postJsonObject): name += '📧' + likesCount = noOfLikes(postJsonObject) + if likesCount > 10: + likesCount = 10 + for like in range(likesCount): + name += '❤' if len(name) > 16: name = name[:16] else: From 3795cb4ce442ea9459a5326cd2079f1fca2362ae Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 18 Mar 2021 20:11:28 +0000 Subject: [PATCH 34/87] Ensure that space is added after name --- desktop_client.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/desktop_client.py b/desktop_client.py index f4e028b84..e6e90f3c2 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -507,15 +507,25 @@ def _desktopShowBoxJson(boxName: str, boxJson: {}, posStr += ' ' authorActor = postJsonObject['object']['attributedTo'] name = getNicknameFromActor(authorActor) + spaceAdded = False if postJsonObject['object'].get('inReplyTo'): - name += ' ↲' + if not spaceAdded: + spaceAdded = True + name += ' ' + name += '↲' if boxName != 'dm': if isDM(postJsonObject): + if not spaceAdded: + spaceAdded = True + name += ' ' name += '📧' likesCount = noOfLikes(postJsonObject) if likesCount > 10: likesCount = 10 for like in range(likesCount): + if not spaceAdded: + spaceAdded = True + name += ' ' name += '❤' if len(name) > 16: name = name[:16] From e65ad80d5930151bbeb0cece0cd9304408a5a7ab Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 18 Mar 2021 20:56:08 +0000 Subject: [PATCH 35/87] Undoing announces via c2s --- announce.py | 79 +++++++++++++++++++++++++++++++++++++++++++++++ desktop_client.py | 66 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 141 insertions(+), 4 deletions(-) diff --git a/announce.py b/announce.py index d07f855a1..01212264e 100644 --- a/announce.py +++ b/announce.py @@ -266,3 +266,82 @@ def sendAnnounceViaServer(baseDir: str, session, print('DEBUG: c2s POST announce success') return newAnnounceJson + + +def sendUndoAnnounceViaServer(baseDir: str, session, + undoPostJsonObject: {}, + nickname: str, password: str, + domain: str, port: int, + httpPrefix: str, repeatObjectUrl: str, + cachedWebfingers: {}, personCache: {}, + debug: bool, projectVersion: str) -> {}: + """Undo an announce message via c2s + """ + if not session: + print('WARN: No session for sendUndoAnnounceViaServer') + return 6 + + domainFull = getFullDomain(domain, port) + + actor = httpPrefix + '://' + domainFull + '/users/' + nickname + handle = actor.replace('/users/', '/@') + + statusNumber, published = getStatusNumber() + unAnnounceJson = { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': actor + '/statuses/' + str(statusNumber) + '/undo', + 'type': 'Undo', + 'actor': actor, + 'object': undoPostJsonObject['object'] + } + + # lookup the inbox for the To handle + wfRequest = webfingerHandle(session, handle, httpPrefix, + cachedWebfingers, + domain, projectVersion, debug) + if not wfRequest: + if debug: + print('DEBUG: undo announce webfinger failed for ' + handle) + return 1 + if not isinstance(wfRequest, dict): + print('WARN: undo announce webfinger for ' + handle + + ' did not return a dict. ' + str(wfRequest)) + return 1 + + postToBox = 'outbox' + + # get the actor inbox for the To handle + (inboxUrl, pubKeyId, pubKey, fromPersonId, + sharedInbox, avatarUrl, + displayName) = getPersonBox(baseDir, session, wfRequest, + personCache, + projectVersion, httpPrefix, + nickname, domain, + postToBox, 73528) + + if not inboxUrl: + if debug: + print('DEBUG: undo announce no ' + postToBox + + ' was found for ' + handle) + return 3 + if not fromPersonId: + if debug: + print('DEBUG: undo announce no actor was found for ' + handle) + return 4 + + authHeader = createBasicAuthHeader(nickname, password) + + headers = { + 'host': domain, + 'Content-type': 'application/json', + 'Authorization': authHeader + } + postResult = postJson(session, unAnnounceJson, [], inboxUrl, + headers, 30, True) + if not postResult: + print('WARN: undo announce not posted') + + if debug: + print('DEBUG: c2s POST undo announce success') + + return unAnnounceJson diff --git a/desktop_client.py b/desktop_client.py index e6e90f3c2..79108e14b 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -32,6 +32,7 @@ from follow import sendUnfollowRequestViaServer from posts import sendPostViaServer from posts import c2sBoxJson from announce import sendAnnounceViaServer +from announce import sendUndoAnnounceViaServer from pgp import pgpDecrypt from pgp import hasLocalPGPkey from pgp import pgpEncryptToActor @@ -994,7 +995,15 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, screenreader, systemLanguage, espeak) print('') - elif commandStr == 'like': + elif commandStr == 'like' or commandStr.startswith('like '): + currIndex = 0 + if ' ' in commandStr: + postIndex = commandStr.split(' ')[-1].strip() + if postIndex.isdigit(): + currIndex = int(postIndex) + if currIndex > 0 and boxJson: + postJsonObject = \ + _desktopGetBoxPostObject(boxJson, currIndex) if postJsonObject: if postJsonObject.get('id'): likeActor = postJsonObject['object']['attributedTo'] @@ -1012,6 +1021,14 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, False, __version__) print('') elif commandStr == 'unlike' or commandStr == 'undo like': + currIndex = 0 + if ' ' in commandStr: + postIndex = commandStr.split(' ')[-1].strip() + if postIndex.isdigit(): + currIndex = int(postIndex) + if currIndex > 0 and boxJson: + postJsonObject = \ + _desktopGetBoxPostObject(boxJson, currIndex) if postJsonObject: if postJsonObject.get('id'): unlikeActor = postJsonObject['object']['attributedTo'] @@ -1029,9 +1046,17 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, cachedWebfingers, personCache, False, __version__) print('') - elif (commandStr == 'announce' or - commandStr == 'boost' or - commandStr == 'retweet'): + elif (commandStr.startswith('announce') or + commandStr.startswith('boost') or + commandStr.startswith('retweet')): + currIndex = 0 + if ' ' in commandStr: + postIndex = commandStr.split(' ')[-1].strip() + if postIndex.isdigit(): + currIndex = int(postIndex) + if currIndex > 0 and boxJson: + postJsonObject = \ + _desktopGetBoxPostObject(boxJson, currIndex) if postJsonObject: if postJsonObject.get('id'): postId = postJsonObject['id'] @@ -1050,6 +1075,39 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, cachedWebfingers, personCache, True, __version__) print('') + elif (commandStr.startswith('unannounce') or + commandStr.startswith('undo announce') or + commandStr.startswith('unboost') or + commandStr.startswith('undo boost') or + commandStr.startswith('undo retweet')): + currIndex = 0 + if ' ' in commandStr: + postIndex = commandStr.split(' ')[-1].strip() + if postIndex.isdigit(): + currIndex = int(postIndex) + if currIndex > 0 and boxJson: + postJsonObject = \ + _desktopGetBoxPostObject(boxJson, currIndex) + if postJsonObject: + if postJsonObject.get('id'): + postId = postJsonObject['id'] + announceActor = \ + postJsonObject['object']['attributedTo'] + sayStr = 'Undoing announce post by ' + \ + getNicknameFromActor(announceActor) + _sayCommand(sayStr, sayStr, + screenreader, + systemLanguage, espeak) + sessionAnnounce = createSession(proxyType) + sendUndoAnnounceViaServer(baseDir, sessionAnnounce, + postJsonObject, + nickname, password, + domain, port, + httpPrefix, postId, + cachedWebfingers, + personCache, + True, __version__) + print('') elif commandStr.startswith('follow '): followHandle = commandStr.replace('follow ', '').strip() if followHandle.startswith('@'): From 014aacedc17feabb46934f76ab7991091d3a06ce Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 18 Mar 2021 22:16:01 +0000 Subject: [PATCH 36/87] Time format --- desktop_client.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/desktop_client.py b/desktop_client.py index 79108e14b..8ba488828 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -461,6 +461,18 @@ def _desktopGetBoxPostObject(boxJson: {}, index: int) -> {}: return None +def _formatPublished(published: str) -> str: + """Formats the published time for display on timeline + """ + dateStr = published.split('T')[0] + monthStr = dateStr.split('-')[1] + dayStr = dateStr.split('-')[2] + timeStr = published.split('T')[1] + hourStr = timeStr.split(':')[0] + minStr = timeStr.split(':')[1] + return monthStr + '-' + dayStr + ' ' + hourStr + ':' + minStr + 'Z' + + def _desktopShowBoxJson(boxName: str, boxJson: {}, screenreader: str, systemLanguage: str, espeak, pageNumber=1, @@ -502,7 +514,7 @@ def _desktopShowBoxJson(boxName: str, boxJson: {}, continue if not postJsonObject['object'].get('content'): continue - published = postJsonObject['published'].replace('T', ' ') + published = _formatPublished(postJsonObject['published']) posStr = str(ctr) while len(posStr) < 2: posStr += ' ' From e4504593b68a131dda3d0c27b7cfc0849119f911 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 18 Mar 2021 23:25:27 +0000 Subject: [PATCH 37/87] Don't send actor updates to self --- posts.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/posts.py b/posts.py index af0505a7b..ed9e3b872 100644 --- a/posts.py +++ b/posts.py @@ -2511,6 +2511,14 @@ def sendToNamedAddresses(session, baseDir: str, toDomain, toPort = getDomainFromActor(address) if not toDomain: continue + # Don't send profile/actor updates to yourself + if isProfileUpdate: + if nickname == toNickname and \ + domainFull == toDomainFull: + if debug: + print('Not sending profile update to self. ' + + nickname + '@' + domainFull) + continue if debug: domainFull = getFullDomain(domain, port) toDomainFull = getFullDomain(toDomain, toPort) From f1e5db1c1846bd381bb6db423ff4a5f1975fa574 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 18 Mar 2021 23:39:54 +0000 Subject: [PATCH 38/87] Assign variable --- posts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/posts.py b/posts.py index ed9e3b872..c70f62e42 100644 --- a/posts.py +++ b/posts.py @@ -2427,8 +2427,8 @@ def sendToNamedAddresses(session, baseDir: str, return if not postJsonObject.get('object'): return + isProfileUpdate = False if isinstance(postJsonObject['object'], dict): - isProfileUpdate = False # for actor updates there is no 'to' within the object if postJsonObject['object'].get('type') and postJsonObject.get('type'): if (postJsonObject['type'] == 'Update' and From 4dcbbc44e977de5337e2d06a48abf017770c8f72 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 19 Mar 2021 10:14:57 +0000 Subject: [PATCH 39/87] Fix unit tests --- desktop_client.py | 6 ++++++ posts.py | 2 ++ speaker.py | 32 -------------------------------- 3 files changed, 8 insertions(+), 32 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index 8ba488828..e552c2646 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -14,6 +14,7 @@ import select import webbrowser import urllib.parse from random import randint +from utils import getFullDomain from utils import isDM from utils import loadTranslationsFromFile from utils import removeHtml @@ -784,6 +785,11 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, newDMsExist = False pgpKeyUpload = False + # NOTE: These are dummy calls to make unit tests pass + # they should be removed later + _desktopNotification("", "test", "message") + _playNotificationSound("test83639") + sayStr = indent + 'Loading translations file' _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) diff --git a/posts.py b/posts.py index c70f62e42..6f41d3a8d 100644 --- a/posts.py +++ b/posts.py @@ -2513,6 +2513,8 @@ def sendToNamedAddresses(session, baseDir: str, continue # Don't send profile/actor updates to yourself if isProfileUpdate: + domainFull = getFullDomain(domain, port) + toDomainFull = getFullDomain(toDomain, toPort) if nickname == toNickname and \ domainFull == toDomainFull: if debug: diff --git a/speaker.py b/speaker.py index dd46c7bbd..db2425618 100644 --- a/speaker.py +++ b/speaker.py @@ -255,38 +255,6 @@ def _removeEmojiFromText(sayText: str) -> str: return sayText.replace(' ', ' ').strip() -def getSpeakerFromServer(baseDir: str, session, - nickname: str, password: str, - domain: str, port: int, - httpPrefix: str, - debug: bool, projectVersion: str) -> {}: - """Returns some json which contains the latest inbox - entry in a minimal format suitable for a text-to-speech reader - """ - if not session: - print('WARN: No session for getSpeakerFromServer') - return 6 - - domainFull = getFullDomain(domain, port) - - authHeader = createBasicAuthHeader(nickname, password) - - headers = { - 'host': domain, - 'Content-type': 'application/json', - 'Authorization': authHeader - } - - url = \ - httpPrefix + '://' + \ - domainFull + '/users/' + nickname + '/speaker' - - speakerJson = \ - getJson(session, url, headers, None, debug, - __version__, httpPrefix, domain, 20, True) - return speakerJson - - def _speakerEndpointJson(displayName: str, summary: str, content: str, sayContent: str, imageDescription: str, From 7b7f182a2aa5c55206aaa8624ae3272678af7b63 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 19 Mar 2021 11:59:14 +0000 Subject: [PATCH 40/87] Tidying --- desktop_client.py | 68 +++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index e552c2646..47ded73dd 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -474,11 +474,11 @@ def _formatPublished(published: str) -> str: return monthStr + '-' + dayStr + ' ' + hourStr + ':' + minStr + 'Z' -def _desktopShowBoxJson(boxName: str, boxJson: {}, - screenreader: str, systemLanguage: str, espeak, - pageNumber=1, - newReplies=False, - newDMs=False) -> bool: +def _desktopShowBox(boxName: str, boxJson: {}, + screenreader: str, systemLanguage: str, espeak, + pageNumber=1, + newReplies=False, + newDMs=False) -> bool: """Shows online timeline """ indent = ' ' @@ -830,11 +830,11 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, timelineFirstId = _getFirstItemId(boxJson) if timelineFirstId != prevTimelineFirstId: _desktopClearScreen() - _desktopShowBoxJson(currTimeline, boxJson, - None, systemLanguage, espeak, - pageNumber, - newRepliesExist, - newDMsExist) + _desktopShowBox(currTimeline, boxJson, + None, systemLanguage, espeak, + pageNumber, + newRepliesExist, + newDMsExist) prevTimelineFirstId = timelineFirstId # wait for a while, or until a key is pressed @@ -864,10 +864,10 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, currTimeline, pageNumber, debug) if boxJson: - _desktopShowBoxJson(currTimeline, boxJson, - screenreader, systemLanguage, espeak, - pageNumber, - newRepliesExist, newDMsExist) + _desktopShowBox(currTimeline, boxJson, + screenreader, systemLanguage, espeak, + pageNumber, + newRepliesExist, newDMsExist) newDMsExist = False elif commandStr.startswith('show rep'): pageNumber = 1 @@ -879,10 +879,10 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, currTimeline, pageNumber, debug) if boxJson: - _desktopShowBoxJson(currTimeline, boxJson, - screenreader, systemLanguage, espeak, - pageNumber, - newRepliesExist, newDMsExist) + _desktopShowBox(currTimeline, boxJson, + screenreader, systemLanguage, espeak, + pageNumber, + newRepliesExist, newDMsExist) # Turn off the replies indicator newRepliesExist = False elif commandStr.startswith('show sen'): @@ -895,10 +895,10 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, currTimeline, pageNumber, debug) if boxJson: - _desktopShowBoxJson(currTimeline, boxJson, - screenreader, systemLanguage, espeak, - pageNumber, - newRepliesExist, newDMsExist) + _desktopShowBox(currTimeline, boxJson, + screenreader, systemLanguage, espeak, + pageNumber, + newRepliesExist, newDMsExist) elif (commandStr == 'show' or commandStr.startswith('show in') or commandStr == 'clear'): pageNumber = 1 @@ -910,10 +910,10 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, currTimeline, pageNumber, debug) if boxJson: - _desktopShowBoxJson(currTimeline, boxJson, - screenreader, systemLanguage, espeak, - pageNumber, - newRepliesExist, newDMsExist) + _desktopShowBox(currTimeline, boxJson, + screenreader, systemLanguage, espeak, + pageNumber, + newRepliesExist, newDMsExist) elif commandStr.startswith('next'): pageNumber += 1 prevTimelineFirstId = '' @@ -923,10 +923,10 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, currTimeline, pageNumber, debug) if boxJson: - _desktopShowBoxJson(currTimeline, boxJson, - screenreader, systemLanguage, espeak, - pageNumber, - newRepliesExist, newDMsExist) + _desktopShowBox(currTimeline, boxJson, + screenreader, systemLanguage, espeak, + pageNumber, + newRepliesExist, newDMsExist) elif commandStr.startswith('prev'): pageNumber -= 1 if pageNumber < 1: @@ -938,10 +938,10 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, currTimeline, pageNumber, debug) if boxJson: - _desktopShowBoxJson(currTimeline, boxJson, - screenreader, systemLanguage, espeak, - pageNumber, - newRepliesExist, newDMsExist) + _desktopShowBox(currTimeline, boxJson, + screenreader, systemLanguage, espeak, + pageNumber, + newRepliesExist, newDMsExist) elif commandStr.startswith('read ') or commandStr == 'read': if commandStr == 'read': postIndexStr = '1' From 4de012144a4c47eae68537f7fe29309d7f892e60 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 19 Mar 2021 13:54:30 +0000 Subject: [PATCH 41/87] Opening announces in the desaktop client --- desktop_client.py | 146 ++++++++++++++++++++++++++++++++++++++-------- speaker.py | 3 - 2 files changed, 121 insertions(+), 28 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index 47ded73dd..be4121aa4 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -14,7 +14,6 @@ import select import webbrowser import urllib.parse from random import randint -from utils import getFullDomain from utils import isDM from utils import loadTranslationsFromFile from utils import removeHtml @@ -32,6 +31,7 @@ from follow import sendFollowRequestViaServer from follow import sendUnfollowRequestViaServer from posts import sendPostViaServer from posts import c2sBoxJson +from posts import downloadAnnounce from announce import sendAnnounceViaServer from announce import sendUndoAnnounceViaServer from pgp import pgpDecrypt @@ -389,7 +389,8 @@ def _textOnlyContent(content: str) -> str: return removeHtml(content) -def _readLocalBoxPost(baseDir: str, boxName: str, +def _readLocalBoxPost(session, nickname: str, domain: str, + httpPrefix: str, baseDir: str, boxName: str, pageNumber: int, index: int, boxJson: {}, systemLanguage: str, screenreader: str, espeak, @@ -398,21 +399,59 @@ def _readLocalBoxPost(baseDir: str, boxName: str, Returns the speaker json """ if _timelineIsEmpty(boxJson): - return + return {} postJsonObject = _desktopGetBoxPostObject(boxJson, index) if not postJsonObject: - return - actor = postJsonObject['object']['attributedTo'] - nameStr = getNicknameFromActor(actor) + return {} gender = 'They/Them' - content = _textOnlyContent(postJsonObject['object']['content']) sayStr = 'Reading ' + boxName + ' post ' + str(index) + \ ' from page ' + str(pageNumber) + '.' sayStr2 = sayStr.replace(' dm ', ' DM ') _sayCommand(sayStr, sayStr2, screenreader, systemLanguage, espeak) + if postJsonObject['type'] == 'Announce': + actor = postJsonObject['actor'] + nameStr = getNicknameFromActor(actor) + recentPostsCache = {} + allowLocalNetworkAccess = False + YTReplacementDomain = None + postJsonObject2 = \ + downloadAnnounce(session, baseDir, + httpPrefix, + nickname, domain, + postJsonObject, + __version__, translate, + YTReplacementDomain, + allowLocalNetworkAccess, + recentPostsCache, False) + if postJsonObject2: + if postJsonObject2.get('object'): + if postJsonObject2['object'].get('attributedTo') and \ + postJsonObject2['object'].get('content'): + actor = postJsonObject2['object']['attributedTo'] + nameStr += ' ' + translate['announces'] + ' ' + \ + getNicknameFromActor(actor) + sayStr = nameStr + _sayCommand(sayStr, sayStr, screenreader, + systemLanguage, espeak) + if screenreader: + time.sleep(2) + content = \ + _textOnlyContent(postJsonObject2['object']['content']) + messageStr, detectedLinks = \ + speakableText(baseDir, content, translate) + sayStr = content + _sayCommand(sayStr, messageStr, screenreader, + systemLanguage, espeak) + return postJsonObject2 + return {} + + actor = postJsonObject['object']['attributedTo'] + nameStr = getNicknameFromActor(actor) + content = _textOnlyContent(postJsonObject['object']['content']) + if isPGPEncrypted(content): sayStr = 'Encrypted message. Please enter your passphrase.' _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) @@ -420,7 +459,7 @@ def _readLocalBoxPost(baseDir: str, boxName: str, if isPGPEncrypted(content): sayStr = 'Message could not be decrypted' _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) - return + return {} content = _safeMessage(content) messageStr, detectedLinks = speakableText(baseDir, content, translate) @@ -448,8 +487,17 @@ def _desktopGetBoxPostObject(boxJson: {}, index: int) -> {}: """ ctr = 0 for postJsonObject in boxJson['orderedItems']: + if not postJsonObject.get('type'): + continue if not postJsonObject.get('object'): continue + if postJsonObject['type'] == 'Announce': + if not isinstance(postJsonObject['object'], str): + continue + ctr += 1 + if ctr == index: + return postJsonObject + continue if not isinstance(postJsonObject['object'], dict): continue if not postJsonObject['object'].get('published'): @@ -474,6 +522,17 @@ def _formatPublished(published: str) -> str: return monthStr + '-' + dayStr + ' ' + hourStr + ':' + minStr + 'Z' +def _padToWidth(content: str, width: int) -> str: + """Pads the given string to the given width + """ + if len(content) > width: + content = content[:width] + else: + while len(content) < width: + content += ' ' + return content + + def _desktopShowBox(boxName: str, boxJson: {}, screenreader: str, systemLanguage: str, espeak, pageNumber=1, @@ -481,6 +540,9 @@ def _desktopShowBox(boxName: str, boxJson: {}, newDMs=False) -> bool: """Shows online timeline """ + numberWidth = 2 + nameWidth = 16 + contentWidth = 50 indent = ' ' # title @@ -507,6 +569,29 @@ def _desktopShowBox(boxName: str, boxJson: {}, ctr = 1 for postJsonObject in boxJson['orderedItems']: + if not postJsonObject.get('type'): + continue + if postJsonObject['type'] == 'Announce': + if postJsonObject.get('actor') and \ + postJsonObject.get('object'): + if isinstance(postJsonObject['object'], str): + authorActor = postJsonObject['actor'] + name = getNicknameFromActor(authorActor) + ' ⮌' + name = _padToWidth(name, nameWidth) + ctrStr = str(ctr) + posStr = _padToWidth(ctrStr, numberWidth) + published = _formatPublished(postJsonObject['published']) + announcedNickname = \ + getNicknameFromActor(postJsonObject['object']) + announcedDomain, announcedPort = \ + getDomainFromActor(postJsonObject['object']) + announcedHandle = announcedNickname + '@' + announcedDomain + print(indent + str(posStr) + ' | ' + name + ' | ' + + published + ' | ' + + _padToWidth(announcedHandle, contentWidth)) + ctr += 1 + continue + if not postJsonObject.get('object'): continue if not isinstance(postJsonObject['object'], dict): @@ -515,12 +600,13 @@ def _desktopShowBox(boxName: str, boxJson: {}, continue if not postJsonObject['object'].get('content'): continue - published = _formatPublished(postJsonObject['published']) - posStr = str(ctr) - while len(posStr) < 2: - posStr += ' ' + ctrStr = str(ctr) + posStr = _padToWidth(ctrStr, numberWidth) + authorActor = postJsonObject['object']['attributedTo'] name = getNicknameFromActor(authorActor) + + # append icons to the end of the name spaceAdded = False if postJsonObject['object'].get('inReplyTo'): if not spaceAdded: @@ -541,21 +627,16 @@ def _desktopShowBox(boxName: str, boxJson: {}, spaceAdded = True name += ' ' name += '❤' - if len(name) > 16: - name = name[:16] - else: - while len(name) < 16: - name += ' ' + name = _padToWidth(name, nameWidth) + + published = _formatPublished(postJsonObject['published']) + content = _textOnlyContent(postJsonObject['object']['content']) if isPGPEncrypted(content): content = '🔒' + content elif '://' in content: content = '🔗' + content - if len(content) > 40: - content = content[:40] - else: - while len(content) < 40: - content += ' ' + content = _padToWidth(content, contentWidth) print(indent + str(posStr) + ' | ' + name + ' | ' + published + ' | ' + content) ctr += 1 @@ -761,8 +842,6 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, postJsonObject = {} originalScreenReader = screenreader - # domainFull = getFullDomain(domain, port) - # actor = httpPrefix + '://' + domainFull + '/users/' + nickname # prevSay = '' # prevCalendar = False # prevFollow = False @@ -950,7 +1029,8 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, if boxJson and postIndexStr.isdigit(): postIndex = int(postIndexStr) postJsonObject = \ - _readLocalBoxPost(baseDir, currTimeline, + _readLocalBoxPost(session, nickname, domain, + httpPrefix, baseDir, currTimeline, pageNumber, postIndex, boxJson, systemLanguage, screenreader, espeak, translate) @@ -1247,6 +1327,22 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, if currIndex > 0 and boxJson: postJsonObject = \ _desktopGetBoxPostObject(boxJson, currIndex) + if postJsonObject: + if postJsonObject['type'] == 'Announce': + recentPostsCache = {} + allowLocalNetworkAccess = False + YTReplacementDomain = None + postJsonObject2 = \ + downloadAnnounce(session, baseDir, + httpPrefix, + nickname, domain, + postJsonObject, + __version__, translate, + YTReplacementDomain, + allowLocalNetworkAccess, + recentPostsCache, False) + if postJsonObject2: + postJsonObject = postJsonObject2 if postJsonObject: content = postJsonObject['object']['content'] messageStr, detectedLinks = \ diff --git a/speaker.py b/speaker.py index db2425618..d6d66fbca 100644 --- a/speaker.py +++ b/speaker.py @@ -10,8 +10,6 @@ import os import html import random import urllib.parse -from auth import createBasicAuthHeader -from session import getJson from utils import isDM from utils import isReply from utils import camelCaseSplit @@ -22,7 +20,6 @@ from utils import getDisplayName from utils import removeHtml from utils import loadJson from utils import saveJson -from utils import getFullDomain from utils import isPGPEncrypted from content import htmlReplaceQuoteMarks From 6c460743b8c05cb7f998ef5998812eaec5aca653 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 19 Mar 2021 14:49:40 +0000 Subject: [PATCH 42/87] Tidying --- desktop_client.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/desktop_client.py b/desktop_client.py index be4121aa4..788027456 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -389,6 +389,35 @@ def _textOnlyContent(content: str) -> str: return removeHtml(content) +def _getImageDescription(postJsonObject: {}) -> str: + """Returns a image description/s on a post + """ + imageDescription = '' + if not postJsonObject['object'].get('attachment'): + return imageDescription + + attachList = postJsonObject['object']['attachment'] + if not isinstance(attachList, list): + return imageDescription + + # for each attachment + for img in attachList: + if not isinstance(img, dict): + continue + if not img.get('name'): + continue + if not isinstance(img['name'], str): + continue + messageStr = img['name'] + if messageStr: + messageStr = messageStr.strip() + if not messageStr.endswith('.'): + imageDescription += messageStr + '. ' + else: + imageDescription += messageStr + ' ' + return imageDescription + + def _readLocalBoxPost(session, nickname: str, domain: str, httpPrefix: str, baseDir: str, boxName: str, pageNumber: int, index: int, boxJson: {}, @@ -440,6 +469,7 @@ def _readLocalBoxPost(session, nickname: str, domain: str, time.sleep(2) content = \ _textOnlyContent(postJsonObject2['object']['content']) + content += _getImageDescription(postJsonObject2) messageStr, detectedLinks = \ speakableText(baseDir, content, translate) sayStr = content @@ -451,6 +481,7 @@ def _readLocalBoxPost(session, nickname: str, domain: str, actor = postJsonObject['object']['attributedTo'] nameStr = getNicknameFromActor(actor) content = _textOnlyContent(postJsonObject['object']['content']) + content += _getImageDescription(postJsonObject) if isPGPEncrypted(content): sayStr = 'Encrypted message. Please enter your passphrase.' From 25f16824dd0df645fd2f44b11b4b552e57b6deee Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 19 Mar 2021 15:28:16 +0000 Subject: [PATCH 43/87] Show outbox --- desktop_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/desktop_client.py b/desktop_client.py index 788027456..258817154 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -995,7 +995,8 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, newRepliesExist, newDMsExist) # Turn off the replies indicator newRepliesExist = False - elif commandStr.startswith('show sen'): + elif (commandStr.startswith('show sen') or + commandStr.startswith('show out')): pageNumber = 1 prevTimelineFirstId = '' currTimeline = 'outbox' From 2c33d55b45fb0936e2d7db9d68b9f67727afd7d6 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 19 Mar 2021 15:31:16 +0000 Subject: [PATCH 44/87] Refresh session if nothing returned --- desktop_client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/desktop_client.py b/desktop_client.py index 258817154..8de625a06 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -946,6 +946,8 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, newRepliesExist, newDMsExist) prevTimelineFirstId = timelineFirstId + else: + session = createSession(proxyType) # wait for a while, or until a key is pressed if noKeyPress: From e1eab5ab2421f2f204ae3feba8fe5b8e3c6648c0 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 19 Mar 2021 15:39:16 +0000 Subject: [PATCH 45/87] Replies timeline in desktop client --- desktop_client.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index 8de625a06..fe485d33d 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -581,7 +581,11 @@ def _desktopShowBox(boxName: str, boxJson: {}, _desktopShowBanner() notificationIcons = '' - titleStr = '\33[7m' + boxName.upper() + '\33[0m' + if boxName.startswith('tl'): + titleStr = boxName[2:] + else: + titleStr = boxName + titleStr = '\33[7m' + titleStr.upper() + '\33[0m' # titleStr += ' page ' + str(pageNumber) if notificationIcons: while len(titleStr) < 95 - len(notificationIcons): @@ -984,7 +988,7 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, elif commandStr.startswith('show rep'): pageNumber = 1 prevTimelineFirstId = '' - currTimeline = 'replies' + currTimeline = 'tlreplies' boxJson = c2sBoxJson(baseDir, session, nickname, password, domain, port, httpPrefix, From 117ed081f3c71a6de6e60a6958dec381b1c97529 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 19 Mar 2021 16:10:14 +0000 Subject: [PATCH 46/87] Replies timeline strings --- desktop_client.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index fe485d33d..d01a6526f 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -582,10 +582,10 @@ def _desktopShowBox(boxName: str, boxJson: {}, notificationIcons = '' if boxName.startswith('tl'): - titleStr = boxName[2:] + boxNameStr = boxName[2:] else: - titleStr = boxName - titleStr = '\33[7m' + titleStr.upper() + '\33[0m' + boxNameStr = boxName + titleStr = '\33[7m' + boxNameStr.upper() + '\33[0m' # titleStr += ' page ' + str(pageNumber) if notificationIcons: while len(titleStr) < 95 - len(notificationIcons): @@ -594,7 +594,7 @@ def _desktopShowBox(boxName: str, boxJson: {}, print(indent + titleStr + '\n') if _timelineIsEmpty(boxJson): - boxStr = boxName + boxStr = boxNameStr if boxName == 'dm': boxStr = 'DM' sayStr = indent + 'You have no ' + boxStr + ' posts yet.' @@ -679,12 +679,12 @@ def _desktopShowBox(boxName: str, boxJson: {}, print('') # say the post number range - sayStr = indent + boxName + ' page ' + str(pageNumber) + \ + sayStr = indent + boxNameStr + ' page ' + str(pageNumber) + \ ' containing ' + str(ctr - 1) + ' posts. ' if newDMs and boxName != 'dm': sayStr += \ 'Use \33[3mshow dm\33[0m to view direct messages.' - elif newReplies and boxName != 'replies': + elif newReplies and boxName != 'tlreplies': sayStr += \ 'Use \33[3mshow replies\33[0m to view reply posts.' else: From 99bfa497735356d03540cfab92b32f59a213bf31 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 19 Mar 2021 17:23:30 +0000 Subject: [PATCH 47/87] Show replying link --- desktop_client.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index d01a6526f..7bbd58367 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -435,7 +435,10 @@ def _readLocalBoxPost(session, nickname: str, domain: str, return {} gender = 'They/Them' - sayStr = 'Reading ' + boxName + ' post ' + str(index) + \ + boxNameStr = boxName + if boxName.startswith('tl'): + boxNameStr = boxName[2:] + sayStr = 'Reading ' + boxNameStr + ' post ' + str(index) + \ ' from page ' + str(pageNumber) + '.' sayStr2 = sayStr.replace(' dm ', ' DM ') _sayCommand(sayStr, sayStr2, screenreader, systemLanguage, espeak) @@ -503,6 +506,9 @@ def _readLocalBoxPost(session, nickname: str, domain: str, systemLanguage, espeak, nameStr, gender) + if postJsonObject['object'].get('inReplyTo'): + print('Replying to ' + postJsonObject['object']['inReplyTo'] + '\n') + if screenreader: time.sleep(2) @@ -581,7 +587,7 @@ def _desktopShowBox(boxName: str, boxJson: {}, _desktopShowBanner() notificationIcons = '' - if boxName.startswith('tl'): + if boxName.startswith('tl'): boxNameStr = boxName[2:] else: boxNameStr = boxName From 548dbc5147826f728b4d4bde38bde145dcdc22a4 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 19 Mar 2021 21:34:38 +0000 Subject: [PATCH 48/87] Bookmarks timeline --- README_commandline.md | 38 +++++++++++++++--------------- desktop_client.py | 54 ++++++++++++++++++++++++++++--------------- 2 files changed, 54 insertions(+), 38 deletions(-) diff --git a/README_commandline.md b/README_commandline.md index c6b392aec..29f41378f 100644 --- a/README_commandline.md +++ b/README_commandline.md @@ -432,25 +432,25 @@ Or if you have picospeaker installed: The desktop client has a few commands, which may be more convenient than the web interface for some purposes: ``` bash -quit Exit from the desktop client -mute Turn off the screen reader -speak Turn on the screen reader -sounds on Turn on notification sounds -sounds off Turn off notification sounds -rp Repeat the last post -like Like the last post -unlike Unlike the last post -reply Reply to the last post -post Create a new post -post to [handle] Create a new direct message -announce/boost Boost the last post -follow [handle] Make a follow request -unfollow [handle] Stop following the give handle -show dm|sent|inbox|replies Show a timeline -next Next page in the timeline -prev Previous page in the timeline -read [post number] Read a post from a timeline -open [post number] Open web links within a timeline post +quit Exit from the desktop client +mute Turn off the screen reader +speak Turn on the screen reader +sounds on Turn on notification sounds +sounds off Turn off notification sounds +rp Repeat the last post +like Like the last post +unlike Unlike the last post +reply Reply to the last post +post Create a new post +post to [handle] Create a new direct message +announce/boost Boost the last post +follow [handle] Make a follow request +unfollow [handle] Stop following the give handle +show dm|sent|inbox|replies|bookmarks Show a timeline +next Next page in the timeline +prev Previous page in the timeline +read [post number] Read a post from a timeline +open [post number] Open web links within a timeline post ``` If you have a GPG key configured on your local system and are sending a direct message to someone who has a PGP key (the exported key, not just the key ID) set as a tag on their profile then it will try to encrypt the message automatically. So under some conditions end-to-end encryption is possible, such that the instance server only sees ciphertext. Conversely, for arriving direct messages if they are PGP encrypted then the desktop client will try to obtain the relevant public key and decrypt. diff --git a/desktop_client.py b/desktop_client.py index 7bbd58367..97fd97edd 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -48,43 +48,43 @@ def _desktopHelp() -> None: print('') print(indent + 'Commands:') print('') - print(indent + 'quit ' + + print(indent + 'quit ' + 'Exit from the desktop client') - print(indent + 'show dm|sent|inbox|replies ' + + print(indent + 'show dm|sent|inbox|replies|bookmarks ' + 'Show a timeline') - print(indent + 'mute ' + + print(indent + 'mute ' + 'Turn off the screen reader') - print(indent + 'speak ' + + print(indent + 'speak ' + 'Turn on the screen reader') - print(indent + 'sounds on ' + + print(indent + 'sounds on ' + 'Turn on notification sounds') - print(indent + 'sounds off ' + + print(indent + 'sounds off ' + 'Turn off notification sounds') - print(indent + 'rp ' + + print(indent + 'rp ' + 'Repeat the last post') - print(indent + 'like ' + + print(indent + 'like ' + 'Like the last post') - print(indent + 'unlike ' + + print(indent + 'unlike ' + 'Unlike the last post') - print(indent + 'reply ' + + print(indent + 'reply ' + 'Reply to the last post') - print(indent + 'post ' + + print(indent + 'post ' + 'Create a new post') - print(indent + 'post to [handle] ' + + print(indent + 'post to [handle] ' + 'Create a new direct message') - print(indent + 'announce/boost ' + + print(indent + 'announce/boost ' + 'Boost the last post') - print(indent + 'follow [handle] ' + + print(indent + 'follow [handle] ' + 'Make a follow request') - print(indent + 'unfollow [handle] ' + + print(indent + 'unfollow [handle] ' + 'Stop following the give handle') - print(indent + 'next ' + + print(indent + 'next ' + 'Next page in the timeline') - print(indent + 'prev ' + + print(indent + 'prev ' + 'Previous page in the timeline') - print(indent + 'read [post number] ' + + print(indent + 'read [post number] ' + 'Read a post from a timeline') - print(indent + 'open [post number] ' + + print(indent + 'open [post number] ' + 'Open web links within a timeline post') print('') @@ -1007,6 +1007,22 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, newRepliesExist, newDMsExist) # Turn off the replies indicator newRepliesExist = False + elif commandStr.startswith('show b'): + pageNumber = 1 + prevTimelineFirstId = '' + currTimeline = 'tlbookmarks' + boxJson = c2sBoxJson(baseDir, session, + nickname, password, + domain, port, httpPrefix, + currTimeline, pageNumber, + debug) + if boxJson: + _desktopShowBox(currTimeline, boxJson, + screenreader, systemLanguage, espeak, + pageNumber, + newRepliesExist, newDMsExist) + # Turn off the replies indicator + newRepliesExist = False elif (commandStr.startswith('show sen') or commandStr.startswith('show out')): pageNumber = 1 From f4c4e240a5186640437e2bbdbea9b4600bbf162b Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 19 Mar 2021 22:04:57 +0000 Subject: [PATCH 49/87] Bookmark request via c2s --- bookmarks.py | 86 +++++++++++++++++++++++++++++++++++++++++++++++ desktop_client.py | 29 ++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/bookmarks.py b/bookmarks.py index 4749ba505..eb68eb7f2 100644 --- a/bookmarks.py +++ b/bookmarks.py @@ -8,6 +8,8 @@ __status__ = "Production" import os from pprint import pprint +from webfinger import webfingerHandle +from auth import createBasicAuthHeader from utils import hasUsersPath from utils import getFullDomain from utils import removeIdEnding @@ -19,6 +21,8 @@ from utils import locatePost from utils import getCachedPostFilename from utils import loadJson from utils import saveJson +from posts import getPersonBox +from session import postJson def undoBookmarksCollectionEntry(recentPostsCache: {}, @@ -449,3 +453,85 @@ def outboxUndoBookmark(recentPostsCache: {}, messageJson['actor'], domain, debug) if debug: print('DEBUG: post undo bookmarked via c2s - ' + postFilename) + + +def sendBookmarkViaServer(baseDir: str, session, + nickname: str, password: str, + domain: str, fromPort: int, + httpPrefix: str, bookmarkUrl: str, + cachedWebfingers: {}, personCache: {}, + debug: bool, projectVersion: str) -> {}: + """Creates a bookmark via c2s + """ + if not session: + print('WARN: No session for sendBookmarkViaServer') + return 6 + + domainFull = getFullDomain(domain, fromPort) + + actor = httpPrefix + '://' + domainFull + '/users/' + nickname + + newBookmarkJson = { + "@context": "https://www.w3.org/ns/activitystreams", + "type": "Add", + "actor": actor, + "object": { + "type": "Document", + "url": bookmarkUrl + }, + "target": actor + "/tlbookmarks" + } + + handle = httpPrefix + '://' + domainFull + '/@' + nickname + + # lookup the inbox for the To handle + wfRequest = webfingerHandle(session, handle, httpPrefix, + cachedWebfingers, + domain, projectVersion, debug) + if not wfRequest: + if debug: + print('DEBUG: bookmark webfinger failed for ' + handle) + return 1 + if not isinstance(wfRequest, dict): + print('WARN: bookmark webfinger for ' + handle + + ' did not return a dict. ' + str(wfRequest)) + return 1 + + postToBox = 'outbox' + + # get the actor inbox for the To handle + (inboxUrl, pubKeyId, pubKey, fromPersonId, sharedInbox, + avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest, + personCache, + projectVersion, httpPrefix, + nickname, domain, + postToBox, 52594) + + if not inboxUrl: + if debug: + print('DEBUG: bookmark no ' + postToBox + + ' was found for ' + handle) + return 3 + if not fromPersonId: + if debug: + print('DEBUG: bookmark no actor was found for ' + handle) + return 4 + + authHeader = createBasicAuthHeader(nickname, password) + + headers = { + 'host': domain, + 'Content-type': 'application/json', + 'Authorization': authHeader + } + postResult = postJson(session, newBookmarkJson, [], inboxUrl, + headers, 30, True) + if not postResult: + if debug: + print('WARN: POST bookmark failed for c2s to ' + inboxUrl) + return 5 + + if debug: + print('DEBUG: c2s POST bookmark success') + + return newBookmarkJson diff --git a/desktop_client.py b/desktop_client.py index 97fd97edd..d15f5fdee 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -39,6 +39,7 @@ from pgp import hasLocalPGPkey from pgp import pgpEncryptToActor from pgp import pgpPublicKeyUpload from like import noOfLikes +from bookmarks import sendBookmarkViaServer def _desktopHelp() -> None: @@ -1178,6 +1179,34 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, cachedWebfingers, personCache, False, __version__) print('') + elif (commandStr == 'bookmark' or + commandStr == 'bm' or + commandStr.startswith('bookmark ') or + commandStr.startswith('bm ')): + currIndex = 0 + if ' ' in commandStr: + postIndex = commandStr.split(' ')[-1].strip() + if postIndex.isdigit(): + currIndex = int(postIndex) + if currIndex > 0 and boxJson: + postJsonObject = \ + _desktopGetBoxPostObject(boxJson, currIndex) + if postJsonObject: + if postJsonObject.get('id'): + likeActor = postJsonObject['object']['attributedTo'] + sayStr = 'Bookmarking post by ' + \ + getNicknameFromActor(likeActor) + _sayCommand(sayStr, sayStr, + screenreader, + systemLanguage, espeak) + sessionLike = createSession(proxyType) + sendBookmarkViaServer(baseDir, sessionLike, + nickname, password, + domain, port, httpPrefix, + postJsonObject['id'], + cachedWebfingers, personCache, + False, __version__) + print('') elif commandStr == 'unlike' or commandStr == 'undo like': currIndex = 0 if ' ' in commandStr: From 6f196d5fc30da37a994dde2d3831155489f007e4 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 19 Mar 2021 22:11:45 +0000 Subject: [PATCH 50/87] Undo bookmark request via c2s --- bookmarks.py | 82 +++++++++++++++++++++++++++++++++++++++++++++++ desktop_client.py | 32 ++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/bookmarks.py b/bookmarks.py index eb68eb7f2..084b938cf 100644 --- a/bookmarks.py +++ b/bookmarks.py @@ -535,3 +535,85 @@ def sendBookmarkViaServer(baseDir: str, session, print('DEBUG: c2s POST bookmark success') return newBookmarkJson + + +def sendUndoBookmarkViaServer(baseDir: str, session, + nickname: str, password: str, + domain: str, fromPort: int, + httpPrefix: str, bookmarkUrl: str, + cachedWebfingers: {}, personCache: {}, + debug: bool, projectVersion: str) -> {}: + """Removes a bookmark via c2s + """ + if not session: + print('WARN: No session for sendUndoBookmarkViaServer') + return 6 + + domainFull = getFullDomain(domain, fromPort) + + actor = httpPrefix + '://' + domainFull + '/users/' + nickname + + newBookmarkJson = { + "@context": "https://www.w3.org/ns/activitystreams", + "type": "Remove", + "actor": actor, + "object": { + "type": "Document", + "url": bookmarkUrl + }, + "target": actor + "/tlbookmarks" + } + + handle = httpPrefix + '://' + domainFull + '/@' + nickname + + # lookup the inbox for the To handle + wfRequest = webfingerHandle(session, handle, httpPrefix, + cachedWebfingers, + domain, projectVersion, debug) + if not wfRequest: + if debug: + print('DEBUG: unbookmark webfinger failed for ' + handle) + return 1 + if not isinstance(wfRequest, dict): + print('WARN: unbookmark webfinger for ' + handle + + ' did not return a dict. ' + str(wfRequest)) + return 1 + + postToBox = 'outbox' + + # get the actor inbox for the To handle + (inboxUrl, pubKeyId, pubKey, fromPersonId, sharedInbox, + avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest, + personCache, + projectVersion, httpPrefix, + nickname, domain, + postToBox, 52594) + + if not inboxUrl: + if debug: + print('DEBUG: unbookmark no ' + postToBox + + ' was found for ' + handle) + return 3 + if not fromPersonId: + if debug: + print('DEBUG: unbookmark no actor was found for ' + handle) + return 4 + + authHeader = createBasicAuthHeader(nickname, password) + + headers = { + 'host': domain, + 'Content-type': 'application/json', + 'Authorization': authHeader + } + postResult = postJson(session, newBookmarkJson, [], inboxUrl, + headers, 30, True) + if not postResult: + if debug: + print('WARN: POST unbookmark failed for c2s to ' + inboxUrl) + return 5 + + if debug: + print('DEBUG: c2s POST unbookmark success') + + return newBookmarkJson diff --git a/desktop_client.py b/desktop_client.py index d15f5fdee..fc2ce2452 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -40,6 +40,7 @@ from pgp import pgpEncryptToActor from pgp import pgpPublicKeyUpload from like import noOfLikes from bookmarks import sendBookmarkViaServer +from bookmarks import sendUndoBookmarkViaServer def _desktopHelp() -> None: @@ -1207,6 +1208,37 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, cachedWebfingers, personCache, False, __version__) print('') + elif (commandStr == 'undo bookmark' or + commandStr == 'undo bm' or + commandStr == 'unbookmark' or + commandStr == 'unbm' or + commandStr.startswith('unbookmark ') or + commandStr.startswith('unbm ')): + currIndex = 0 + if ' ' in commandStr: + postIndex = commandStr.split(' ')[-1].strip() + if postIndex.isdigit(): + currIndex = int(postIndex) + if currIndex > 0 and boxJson: + postJsonObject = \ + _desktopGetBoxPostObject(boxJson, currIndex) + if postJsonObject: + if postJsonObject.get('id'): + likeActor = postJsonObject['object']['attributedTo'] + sayStr = 'Unbookmarking post by ' + \ + getNicknameFromActor(likeActor) + _sayCommand(sayStr, sayStr, + screenreader, + systemLanguage, espeak) + sessionLike = createSession(proxyType) + sendUndoBookmarkViaServer(baseDir, sessionLike, + nickname, password, + domain, port, httpPrefix, + postJsonObject['id'], + cachedWebfingers, + personCache, + False, __version__) + print('') elif commandStr == 'unlike' or commandStr == 'undo like': currIndex = 0 if ' ' in commandStr: From 60276e70e90bbebfd5ce9c19c038a06cb18b8bfb Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 20 Mar 2021 09:49:43 +0000 Subject: [PATCH 51/87] Redo inbox bookmark handling --- README_commandline.md | 2 + bookmarks.py | 248 +++++++++++++++++++++++------------------- desktop_client.py | 4 + inbox.py | 186 +++++++++++++++++-------------- outbox.py | 2 +- 5 files changed, 250 insertions(+), 192 deletions(-) diff --git a/README_commandline.md b/README_commandline.md index 29f41378f..be0071834 100644 --- a/README_commandline.md +++ b/README_commandline.md @@ -440,6 +440,8 @@ sounds off Turn off notification sounds rp Repeat the last post like Like the last post unlike Unlike the last post +bookmark Bookmark the last post +unbookmark Unbookmark the last post reply Reply to the last post post Create a new post post to [handle] Create a new direct message diff --git a/bookmarks.py b/bookmarks.py index 084b938cf..b8d1f44d3 100644 --- a/bookmarks.py +++ b/bookmarks.py @@ -345,116 +345,6 @@ def undoBookmark(recentPostsCache: {}, return newUndoBookmarkJson -def outboxBookmark(recentPostsCache: {}, - baseDir: str, httpPrefix: str, - nickname: str, domain: str, port: int, - messageJson: {}, debug: bool) -> None: - """ When a bookmark request is received by the outbox from c2s - """ - if not messageJson.get('type'): - if debug: - print('DEBUG: bookmark - no type') - return - if not messageJson['type'] == 'Bookmark': - if debug: - print('DEBUG: not a bookmark') - return - if not messageJson.get('object'): - if debug: - print('DEBUG: no object in bookmark') - return - if not isinstance(messageJson['object'], str): - if debug: - print('DEBUG: bookmark object is not string') - return - if messageJson.get('to'): - if not isinstance(messageJson['to'], list): - return - if len(messageJson['to']) != 1: - print('WARN: Bookmark should only be sent to one recipient') - return - if messageJson['to'][0] != messageJson['actor']: - print('WARN: Bookmark should be addressed to the same actor') - return - if debug: - print('DEBUG: c2s bookmark request arrived in outbox') - - messageId = removeIdEnding(messageJson['object']) - if ':' in domain: - domain = domain.split(':')[0] - postFilename = locatePost(baseDir, nickname, domain, messageId) - if not postFilename: - if debug: - print('DEBUG: c2s bookmark post not found in inbox or outbox') - print(messageId) - return True - updateBookmarksCollection(recentPostsCache, - baseDir, postFilename, messageId, - messageJson['actor'], domain, debug) - if debug: - print('DEBUG: post bookmarked via c2s - ' + postFilename) - - -def outboxUndoBookmark(recentPostsCache: {}, - baseDir: str, httpPrefix: str, - nickname: str, domain: str, port: int, - messageJson: {}, debug: bool) -> None: - """ When an undo bookmark request is received by the outbox from c2s - """ - if not messageJson.get('type'): - return - if not messageJson['type'] == 'Undo': - return - if not messageJson.get('object'): - return - if not isinstance(messageJson['object'], dict): - if debug: - print('DEBUG: undo bookmark object is not dict') - return - if not messageJson['object'].get('type'): - if debug: - print('DEBUG: undo bookmark - no type') - return - if not messageJson['object']['type'] == 'Bookmark': - if debug: - print('DEBUG: not a undo bookmark') - return - if not messageJson['object'].get('object'): - if debug: - print('DEBUG: no object in undo bookmark') - return - if not isinstance(messageJson['object']['object'], str): - if debug: - print('DEBUG: undo bookmark object is not string') - return - if messageJson.get('to'): - if not isinstance(messageJson['to'], list): - return - if len(messageJson['to']) != 1: - print('WARN: Bookmark should only be sent to one recipient') - return - if messageJson['to'][0] != messageJson['actor']: - print('WARN: Bookmark should be addressed to the same actor') - return - if debug: - print('DEBUG: c2s undo bookmark request arrived in outbox') - - messageId = removeIdEnding(messageJson['object']['object']) - if ':' in domain: - domain = domain.split(':')[0] - postFilename = locatePost(baseDir, nickname, domain, messageId) - if not postFilename: - if debug: - print('DEBUG: c2s undo bookmark post not found in inbox or outbox') - print(messageId) - return True - undoBookmarksCollectionEntry(recentPostsCache, - baseDir, postFilename, messageId, - messageJson['actor'], domain, debug) - if debug: - print('DEBUG: post undo bookmarked via c2s - ' + postFilename) - - def sendBookmarkViaServer(baseDir: str, session, nickname: str, password: str, domain: str, fromPort: int, @@ -617,3 +507,141 @@ def sendUndoBookmarkViaServer(baseDir: str, session, print('DEBUG: c2s POST unbookmark success') return newBookmarkJson + + +def outboxBookmark(recentPostsCache: {}, + baseDir: str, httpPrefix: str, + nickname: str, domain: str, port: int, + messageJson: {}, debug: bool) -> None: + """ When a bookmark request is received by the outbox from c2s + """ + if not messageJson.get('type'): + if debug: + print('DEBUG: bookmark - no type') + return + if not messageJson['type'] == 'Add': + if debug: + print('DEBUG: not a bookmark Add') + return + if not messageJson.get('actor'): + if debug: + print('DEBUG: no actor in bookmark Add') + return + if not messageJson.get('object'): + if debug: + print('DEBUG: no object in bookmark Add') + return + if not messageJson.get('target'): + if debug: + print('DEBUG: no target in bookmark Add') + return + if not isinstance(messageJson['object'], str): + if debug: + print('DEBUG: bookmark Add object is not string') + return + if not isinstance(messageJson['target'], str): + if debug: + print('DEBUG: bookmark Add target is not string') + return + domainFull = getFullDomain(domain, port) + if not messageJson['target'].endswith('://' + domainFull + + '/users/' + nickname + + '/tlbookmarks'): + if debug: + print('DEBUG: bookmark Add target invalid ' + + messageJson['target']) + return + if messageJson['object']['type'] != 'Document': + if debug: + print('DEBUG: bookmark Add type is not Document') + return + if not messageJson['object'].get('url'): + if debug: + print('DEBUG: bookmark Add missing url') + return + if debug: + print('DEBUG: c2s bookmark Add request arrived in outbox') + + messageUrl = removeIdEnding(messageJson['object']['url']) + if ':' in domain: + domain = domain.split(':')[0] + postFilename = locatePost(baseDir, nickname, domain, messageUrl) + if not postFilename: + if debug: + print('DEBUG: c2s like post not found in inbox or outbox') + print(messageUrl) + return True + updateBookmarksCollection(recentPostsCache, + baseDir, postFilename, messageUrl, + messageJson['actor'], domain, debug) + if debug: + print('DEBUG: post bookmarked via c2s - ' + postFilename) + + +def outboxUndoBookmark(recentPostsCache: {}, + baseDir: str, httpPrefix: str, + nickname: str, domain: str, port: int, + messageJson: {}, debug: bool) -> None: + """ When an undo bookmark request is received by the outbox from c2s + """ + if not messageJson.get('type'): + if debug: + print('DEBUG: unbookmark - no type') + return + if not messageJson['type'] == 'Remove': + if debug: + print('DEBUG: not an unbookmark Remove') + return + if not messageJson.get('actor'): + if debug: + print('DEBUG: no actor in unbookmark Remove') + return + if not messageJson.get('object'): + if debug: + print('DEBUG: no object in unbookmark Remove') + return + if not messageJson.get('target'): + if debug: + print('DEBUG: no target in unbookmark Remove') + return + if not isinstance(messageJson['object'], str): + if debug: + print('DEBUG: unbookmark Remove object is not string') + return + if not isinstance(messageJson['target'], str): + if debug: + print('DEBUG: unbookmark Remove target is not string') + return + domainFull = getFullDomain(domain, port) + if not messageJson['target'].endswith('://' + domainFull + + '/users/' + nickname + + '/tlbookmarks'): + if debug: + print('DEBUG: unbookmark Remove target invalid ' + + messageJson['target']) + return + if messageJson['object']['type'] != 'Document': + if debug: + print('DEBUG: unbookmark Remove type is not Document') + return + if not messageJson['object'].get('url'): + if debug: + print('DEBUG: unbookmark Remove missing url') + return + if debug: + print('DEBUG: c2s unbookmark Remove request arrived in outbox') + + messageUrl = removeIdEnding(messageJson['object']['url']) + if ':' in domain: + domain = domain.split(':')[0] + postFilename = locatePost(baseDir, nickname, domain, messageUrl) + if not postFilename: + if debug: + print('DEBUG: c2s unbookmark post not found in inbox or outbox') + print(messageUrl) + return True + updateBookmarksCollection(recentPostsCache, + baseDir, postFilename, messageUrl, + messageJson['actor'], domain, debug) + if debug: + print('DEBUG: post unbookmarked via c2s - ' + postFilename) diff --git a/desktop_client.py b/desktop_client.py index fc2ce2452..d8ede1975 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -68,6 +68,10 @@ def _desktopHelp() -> None: 'Like the last post') print(indent + 'unlike ' + 'Unlike the last post') + print(indent + 'bookmark ' + + 'bookmark the last post') + print(indent + 'unbookmark ' + + 'Unbookmark the last post') print(indent + 'reply ' + 'Reply to the last post') print(indent + 'post ' + diff --git a/inbox.py b/inbox.py index e69605af7..8975a3e74 100644 --- a/inbox.py +++ b/inbox.py @@ -1082,60 +1082,73 @@ def _receiveBookmark(recentPostsCache: {}, debug: bool) -> bool: """Receives a bookmark activity within the POST section of HTTPServer """ - if messageJson['type'] != 'Bookmark': - return False + if not messageJson.get('type'): + if debug: + print('DEBUG: inbox bookmark - no type') + return + if not messageJson['type'] == 'Add': + if debug: + print('DEBUG: not a inbox bookmark Add') + return if not messageJson.get('actor'): if debug: - print('DEBUG: ' + messageJson['type'] + ' has no actor') - return False + print('DEBUG: no actor in inbox bookmark Add') + return if not messageJson.get('object'): if debug: - print('DEBUG: ' + messageJson['type'] + ' has no object') - return False + print('DEBUG: no object in inbox bookmark Add') + return + if not messageJson.get('target'): + if debug: + print('DEBUG: no target in inbox bookmark Add') + return if not isinstance(messageJson['object'], str): if debug: - print('DEBUG: ' + messageJson['type'] + ' object is not a string') - return False - if not messageJson.get('to'): + print('DEBUG: inbox bookmark Add object is not string') + return + if not isinstance(messageJson['target'], str): if debug: - print('DEBUG: ' + messageJson['type'] + ' has no "to" list') - return False - if '/users/' not in messageJson['actor']: - if debug: - print('DEBUG: "users" missing from actor in ' + - messageJson['type']) - return False - if '/statuses/' not in messageJson['object']: - if debug: - print('DEBUG: "statuses" missing from object in ' + - messageJson['type']) - return False - if domain not in handle.split('@')[1]: - if debug: - print('DEBUG: unrecognized domain ' + handle) - return False + print('DEBUG: inbox bookmark Add target is not string') + return domainFull = getFullDomain(domain, port) nickname = handle.split('@')[0] if not messageJson['actor'].endswith(domainFull + '/users/' + nickname): if debug: - print('DEBUG: ' + - 'bookmark actor should be the same as the handle sent to ' + - handle + ' != ' + messageJson['actor']) - return False - if not os.path.isdir(baseDir + '/accounts/' + handle): - print('DEBUG: unknown recipient of bookmark - ' + handle) - # if this post in the outbox of the person? - postFilename = locatePost(baseDir, nickname, domain, messageJson['object']) + print('DEBUG: inbox bookmark Add unexpected actor') + return + if not messageJson['target'].endswith(messageJson['actor'] + + '/tlbookmarks'): + if debug: + print('DEBUG: inbox bookmark Add target invalid ' + + messageJson['target']) + return + if messageJson['object']['type'] != 'Document': + if debug: + print('DEBUG: inbox bookmark Add type is not Document') + return + if not messageJson['object'].get('url'): + if debug: + print('DEBUG: inbox bookmark Add missing url') + return + if '/statuses/' not in messageJson['object']['url']: + if debug: + print('DEBUG: inbox bookmark Add missing statuses un url') + return + if debug: + print('DEBUG: c2s inbox bookmark Add request arrived in outbox') + + messageUrl = removeIdEnding(messageJson['object']['url']) + if ':' in domain: + domain = domain.split(':')[0] + postFilename = locatePost(baseDir, nickname, domain, messageUrl) if not postFilename: if debug: - print('DEBUG: post not found in inbox or outbox') - print(messageJson['object']) + print('DEBUG: c2s inbox like post not found in inbox or outbox') + print(messageUrl) return True - if debug: - print('DEBUG: bookmarked post was found') updateBookmarksCollection(recentPostsCache, baseDir, postFilename, - messageJson['object'], + messageJson['object']['url'], messageJson['actor'], domain, debug) return True @@ -1148,63 +1161,74 @@ def _receiveUndoBookmark(recentPostsCache: {}, debug: bool) -> bool: """Receives an undo bookmark activity within the POST section of HTTPServer """ - if messageJson['type'] != 'Undo': - return False + if not messageJson.get('type'): + if debug: + print('DEBUG: inbox undo bookmark - no type') + return + if not messageJson['type'] == 'Remove': + if debug: + print('DEBUG: not a inbox undo bookmark Remove') + return if not messageJson.get('actor'): - return False + if debug: + print('DEBUG: no actor in inbox undo bookmark Remove') + return if not messageJson.get('object'): - return False - if not isinstance(messageJson['object'], dict): - return False - if not messageJson['object'].get('type'): - return False - if messageJson['object']['type'] != 'Bookmark': - return False - if not messageJson['object'].get('object'): if debug: - print('DEBUG: ' + messageJson['type'] + ' like has no object') - return False - if not isinstance(messageJson['object']['object'], str): + print('DEBUG: no object in inbox undo bookmark Remove') + return + if not messageJson.get('target'): if debug: - print('DEBUG: ' + messageJson['type'] + - ' like object is not a string') - return False - if '/users/' not in messageJson['actor']: + print('DEBUG: no target in inbox undo bookmark Remove') + return + if not isinstance(messageJson['object'], str): if debug: - print('DEBUG: "users" missing from actor in ' + - messageJson['type'] + ' like') - return False - if '/statuses/' not in messageJson['object']['object']: + print('DEBUG: inbox undo bookmark Remove object is not string') + return + if not isinstance(messageJson['target'], str): if debug: - print('DEBUG: "statuses" missing from like object in ' + - messageJson['type']) - return False + print('DEBUG: inbox undo bookmark Remove target is not string') + return domainFull = getFullDomain(domain, port) nickname = handle.split('@')[0] - if domain not in handle.split('@')[1]: - if debug: - print('DEBUG: unrecognized bookmark domain ' + handle) - return False if not messageJson['actor'].endswith(domainFull + '/users/' + nickname): if debug: - print('DEBUG: ' + - 'bookmark actor should be the same as the handle sent to ' + - handle + ' != ' + messageJson['actor']) - return False - if not os.path.isdir(baseDir + '/accounts/' + handle): - print('DEBUG: unknown recipient of bookmark undo - ' + handle) - # if this post in the outbox of the person? - postFilename = locatePost(baseDir, nickname, domain, - messageJson['object']['object']) + print('DEBUG: inbox undo bookmark Remove unexpected actor') + return + if not messageJson['target'].endswith(messageJson['actor'] + + '/tlbookmarks'): + if debug: + print('DEBUG: inbox undo bookmark Remove target invalid ' + + messageJson['target']) + return + if messageJson['object']['type'] != 'Document': + if debug: + print('DEBUG: inbox undo bookmark Remove type is not Document') + return + if not messageJson['object'].get('url'): + if debug: + print('DEBUG: inbox undo bookmark Remove missing url') + return + if '/statuses/' not in messageJson['object']['url']: + if debug: + print('DEBUG: inbox undo bookmark Remove missing statuses un url') + return + if debug: + print('DEBUG: c2s inbox undo bookmark Remove ' + + 'request arrived in outbox') + + messageUrl = removeIdEnding(messageJson['object']['url']) + if ':' in domain: + domain = domain.split(':')[0] + postFilename = locatePost(baseDir, nickname, domain, messageUrl) if not postFilename: if debug: - print('DEBUG: unbookmarked post not found in inbox or outbox') - print(messageJson['object']['object']) + print('DEBUG: c2s inbox like post not found in inbox or outbox') + print(messageUrl) return True - if debug: - print('DEBUG: bookmarked post found. Now undoing.') + undoBookmarksCollectionEntry(recentPostsCache, baseDir, postFilename, - messageJson['object'], + messageJson['object']['url'], messageJson['actor'], domain, debug) return True diff --git a/outbox.py b/outbox.py index 11b3f2dc4..0b04cdf82 100644 --- a/outbox.py +++ b/outbox.py @@ -309,7 +309,7 @@ def postMessageToOutbox(session, translate: {}, permittedOutboxTypes = ('Create', 'Announce', 'Like', 'Follow', 'Undo', 'Update', 'Add', 'Remove', 'Block', 'Delete', - 'Delegate', 'Skill', 'Bookmark', 'Event') + 'Delegate', 'Skill', 'Add', 'Remove', 'Event') if messageJson['type'] not in permittedOutboxTypes: if debug: print('DEBUG: POST to outbox - ' + messageJson['type'] + From d2a32d0d609afa575f0691c0cbca93a089525ebe Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 20 Mar 2021 10:13:59 +0000 Subject: [PATCH 52/87] Tidying --- bookmarks.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bookmarks.py b/bookmarks.py index b8d1f44d3..d881d8e3e 100644 --- a/bookmarks.py +++ b/bookmarks.py @@ -71,8 +71,8 @@ def undoBookmarksCollectionEntry(recentPostsCache: {}, return if not postJsonObject.get('object'): if debug: - pprint(postJsonObject) - print('DEBUG: post ' + objectUrl + ' has no object') + print('DEBUG: bookmarked post has no object ' + + str(postJsonObject)) return if not isinstance(postJsonObject['object'], dict): return @@ -158,11 +158,12 @@ def updateBookmarksCollection(recentPostsCache: {}, if not postJsonObject.get('object'): if debug: - pprint(postJsonObject) - print('DEBUG: post ' + objectUrl + ' has no object') + print('DEBUG: no object in bookmarked post ' + + str(postJsonObject)) return if not objectUrl.endswith('/bookmarks'): objectUrl = objectUrl + '/bookmarks' + # does this post have bookmarks on it from differenent actors? if not postJsonObject['object'].get('bookmarks'): if debug: print('DEBUG: Adding initial bookmarks to ' + objectUrl) From b8fb58b2fbe7333f3b1fbac3086fa2d98457fa35 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 20 Mar 2021 10:16:06 +0000 Subject: [PATCH 53/87] Add bookmarks as a reserved name --- utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utils.py b/utils.py index 0090abd7e..04bde5f5e 100644 --- a/utils.py +++ b/utils.py @@ -1358,7 +1358,8 @@ def _isReservedName(nickname: str) -> bool: 'accounts', 'channels', 'profile', 'u', 'updates', 'repeat', 'announce', 'shares', 'fonts', 'icons', 'avatars', - 'welcome', 'helpimages') + 'welcome', 'helpimages', + 'bookmark', 'bookmarks', 'tlbookmarks') if nickname in reservedNames: return True return False From 01b08104e17bea4ef5357041cda9ee5d3503c466 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 20 Mar 2021 10:18:38 +0000 Subject: [PATCH 54/87] Comment --- posts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/posts.py b/posts.py index 6f41d3a8d..d6bf5f69b 100644 --- a/posts.py +++ b/posts.py @@ -3256,7 +3256,7 @@ def _createBoxIndexed(recentPostsCache: {}, # created by individualPostAsHtml p['hasReplies'] = hasReplies - # Don't show likes, replies, DMs or shares (announces) to + # Don't show likes, replies, bookmarks, DMs or shares (announces) to # unauthorized viewers if not authorized: if p.get('object'): From a3069d0922475875d82222815a5985a5c4291688 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 20 Mar 2021 10:43:52 +0000 Subject: [PATCH 55/87] Add bokkmarks to commandline --- epicyon.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/epicyon.py b/epicyon.py index 4e3971f24..5b2d3fc97 100644 --- a/epicyon.py +++ b/epicyon.py @@ -22,6 +22,8 @@ from person import deactivateAccount from skills import setSkillLevel from roles import setRole from webfinger import webfingerHandle +from bookmarks import sendBookmarkViaServer +from bookmarks import sendUndoBookmarkViaServer from posts import c2sBoxJson from posts import downloadFollowCollection from posts import getPublicPostDomains @@ -417,6 +419,12 @@ parser.add_argument('--favorite', '--like', dest='like', type=str, default=None, help='Like a url') parser.add_argument('--undolike', '--unlike', dest='undolike', type=str, default=None, help='Undo a like of a url') +parser.add_argument('--bookmark', '--bm', dest='bookmark', type=str, + default=None, + help='Bookmark the url of a post') +parser.add_argument('--unbookmark', '--unbm', dest='unbookmark', type=str, + default=None, + help='Undo a bookmark given the url of a post') parser.add_argument('--sendto', dest='sendto', type=str, default=None, help='Address to send a post to') parser.add_argument('--attach', dest='attach', type=str, @@ -1304,6 +1312,62 @@ if args.undolike: time.sleep(1) sys.exit() +if args.bookmark: + if not args.nickname: + print('Specify a nickname with the --nickname option') + sys.exit() + + if not args.password: + args.password = getpass.getpass('Password: ') + if not args.password: + print('Specify a password with the --password option') + sys.exit() + args.password = args.password.replace('\n', '') + + session = createSession(proxyType) + personCache = {} + cachedWebfingers = {} + print('Sending bookmark of ' + args.bookmark) + + sendBookmarkViaServer(baseDir, session, + args.nickname, args.password, + domain, port, + httpPrefix, args.bookmark, + cachedWebfingers, personCache, + True, __version__) + for i in range(10): + # TODO detect send success/fail + time.sleep(1) + sys.exit() + +if args.unbookmark: + if not args.nickname: + print('Specify a nickname with the --nickname option') + sys.exit() + + if not args.password: + args.password = getpass.getpass('Password: ') + if not args.password: + print('Specify a password with the --password option') + sys.exit() + args.password = args.password.replace('\n', '') + + session = createSession(proxyType) + personCache = {} + cachedWebfingers = {} + print('Sending undo bookmark of ' + args.unbookmark) + + sendUndoBookmarkViaServer(baseDir, session, + args.nickname, args.password, + domain, port, + httpPrefix, args.unbookmark, + cachedWebfingers, personCache, + True, __version__) + for i in range(10): + # TODO detect send success/fail + time.sleep(1) + sys.exit() + if args.delete: if not args.nickname: print('Specify a nickname with the --nickname option') From 4af4cf401e87ac4273249fcb3304e761ff584e08 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 20 Mar 2021 10:47:55 +0000 Subject: [PATCH 56/87] Document bookmark commands --- README_commandline.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README_commandline.md b/README_commandline.md index be0071834..bd8be89f0 100644 --- a/README_commandline.md +++ b/README_commandline.md @@ -258,6 +258,22 @@ Or to unblock: python3 epicyon.py --nickname yournick --domain yourdomain --unblock somenick@somedomain --password [c2s password] ``` +## Bookmarking + +You may want to bookmark posts for later viewing or replying. This can be done via c2s with the following: + +``` bash +python3 epicyon.py --nickname yournick --domain yourdomain --bookmark [post URL] --password [c2s password] +``` + +Note that the URL must be that of an ActivityPub post in your timeline. Any other URL will be ignored. + +And to undo the bookmark: + +``` bash +python3 epicyon.py --nickname yournick --domain yourdomain --unbookmark [post URL] --password [c2s password] +``` + ## Filtering on words or phrases Blocking based upon the content of a message containing certain words or phrases is relatively crude and not always effective, but can help to reduce unwanted communications. From eada66269121a762f68b4605625dc3de837dab9a Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 20 Mar 2021 12:00:00 +0000 Subject: [PATCH 57/87] Add a to field to bookmarks --- posts.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/posts.py b/posts.py index d6bf5f69b..5d5a6e9e2 100644 --- a/posts.py +++ b/posts.py @@ -2365,7 +2365,7 @@ def sendSignedJson(postJsonObject: {}, session, baseDir: str, def addToField(activityType: str, postJsonObject: {}, debug: bool) -> ({}, bool): - """The Follow activity doesn't have a 'to' field and so one + """The Follow/Add/Remove activity doesn't have a 'to' field and so one needs to be added so that activity distribution happens in a consistent way Returns true if a 'to' field exists or was added """ @@ -2384,19 +2384,34 @@ def addToField(activityType: str, postJsonObject: {}, if postJsonObject['type'] == activityType: isSameType = True if debug: - print('DEBUG: "to" field assigned to Follow') + print('DEBUG: "to" field assigned to ' + activityType) toAddress = postJsonObject['object'] if '/statuses/' in toAddress: toAddress = toAddress.split('/statuses/')[0] postJsonObject['to'] = [toAddress] toFieldAdded = True elif isinstance(postJsonObject['object'], dict): - if postJsonObject['object'].get('type'): + # add a to field to bookmark add or remove + if postJsonObject.get('type') and \ + postJsonObject.get('actor') and \ + postJsonObject['object'].get('type'): + if postJsonObject['type'] == 'Add' or \ + postJsonObject['type'] == 'Remove': + if postJsonObject['object']['type'] == 'Document': + postJsonObject['to'] = \ + [postJsonObject['actor']] + postJsonObject['object']['to'] = \ + [postJsonObject['actor']] + toFieldAdded = True + + if not toFieldAdded and \ + postJsonObject['object'].get('type'): if postJsonObject['object']['type'] == activityType: isSameType = True if isinstance(postJsonObject['object']['object'], str): if debug: - print('DEBUG: "to" field assigned to Follow') + print('DEBUG: "to" field assigned to ' + + activityType) toAddress = postJsonObject['object']['object'] if '/statuses/' in toAddress: toAddress = toAddress.split('/statuses/')[0] From d9bc4d72aaf59eb56522b518ca0fa12eef3ada36 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 20 Mar 2021 12:06:34 +0000 Subject: [PATCH 58/87] To field can be added for multiple activities --- daemon.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/daemon.py b/daemon.py index 219f2ed28..ee93178ff 100644 --- a/daemon.py +++ b/daemon.py @@ -1260,15 +1260,10 @@ class PubServer(BaseHTTPRequestHandler): originalMessageJson = messageJson.copy() - # For follow activities add a 'to' field, which is a copy - # of the object field - messageJson, toFieldExists = \ - addToField('Follow', messageJson, self.server.debug) - - # For like activities add a 'to' field, which is a copy of - # the actor within the object field - messageJson, toFieldExists = \ - addToField('Like', messageJson, self.server.debug) + addToFieldTypes = ('Follow', 'Like', 'Add', 'Remove') + for addToType in addToFieldTypes: + messageJson, toFieldExists = \ + addToField(addToType, messageJson, self.server.debug) beginSaveTime = time.time() # save the json for later queue processing From 24f9c2e6c6f6a781b9f94917ce276e4ff4362e7f Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 20 Mar 2021 12:20:17 +0000 Subject: [PATCH 59/87] Add to field --- bookmarks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bookmarks.py b/bookmarks.py index d881d8e3e..1cb051a42 100644 --- a/bookmarks.py +++ b/bookmarks.py @@ -366,9 +366,11 @@ def sendBookmarkViaServer(baseDir: str, session, "@context": "https://www.w3.org/ns/activitystreams", "type": "Add", "actor": actor, + "to": [actor], "object": { "type": "Document", - "url": bookmarkUrl + "url": bookmarkUrl, + "to": [actor] }, "target": actor + "/tlbookmarks" } From eabe1ea535334614b8883d3a7d6dee8b9aebca93 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 20 Mar 2021 14:09:07 +0000 Subject: [PATCH 60/87] Tidy bookmark debug --- bookmarks.py | 12 ++-------- inbox.py | 62 +++++++++++++++++++++++----------------------------- 2 files changed, 29 insertions(+), 45 deletions(-) diff --git a/bookmarks.py b/bookmarks.py index 1cb051a42..f696c1d7c 100644 --- a/bookmarks.py +++ b/bookmarks.py @@ -519,12 +519,8 @@ def outboxBookmark(recentPostsCache: {}, """ When a bookmark request is received by the outbox from c2s """ if not messageJson.get('type'): - if debug: - print('DEBUG: bookmark - no type') return - if not messageJson['type'] == 'Add': - if debug: - print('DEBUG: not a bookmark Add') + if messageJson['type'] != 'Add': return if not messageJson.get('actor'): if debug: @@ -588,12 +584,8 @@ def outboxUndoBookmark(recentPostsCache: {}, """ When an undo bookmark request is received by the outbox from c2s """ if not messageJson.get('type'): - if debug: - print('DEBUG: unbookmark - no type') return - if not messageJson['type'] == 'Remove': - if debug: - print('DEBUG: not an unbookmark Remove') + if messageJson['type'] != 'Remove': return if not messageJson.get('actor'): if debug: diff --git a/inbox.py b/inbox.py index 8975a3e74..84241a80b 100644 --- a/inbox.py +++ b/inbox.py @@ -1083,57 +1083,53 @@ def _receiveBookmark(recentPostsCache: {}, """Receives a bookmark activity within the POST section of HTTPServer """ if not messageJson.get('type'): - if debug: - print('DEBUG: inbox bookmark - no type') - return - if not messageJson['type'] == 'Add': - if debug: - print('DEBUG: not a inbox bookmark Add') - return + return False + if messageJson['type'] != 'Add': + return False if not messageJson.get('actor'): if debug: print('DEBUG: no actor in inbox bookmark Add') - return + return False if not messageJson.get('object'): if debug: print('DEBUG: no object in inbox bookmark Add') - return + return False if not messageJson.get('target'): if debug: print('DEBUG: no target in inbox bookmark Add') - return + return False if not isinstance(messageJson['object'], str): if debug: print('DEBUG: inbox bookmark Add object is not string') - return + return False if not isinstance(messageJson['target'], str): if debug: print('DEBUG: inbox bookmark Add target is not string') - return + return False domainFull = getFullDomain(domain, port) nickname = handle.split('@')[0] if not messageJson['actor'].endswith(domainFull + '/users/' + nickname): if debug: print('DEBUG: inbox bookmark Add unexpected actor') - return + return False if not messageJson['target'].endswith(messageJson['actor'] + '/tlbookmarks'): if debug: print('DEBUG: inbox bookmark Add target invalid ' + messageJson['target']) - return + return False if messageJson['object']['type'] != 'Document': if debug: print('DEBUG: inbox bookmark Add type is not Document') - return + return False if not messageJson['object'].get('url'): if debug: print('DEBUG: inbox bookmark Add missing url') - return + return False if '/statuses/' not in messageJson['object']['url']: if debug: print('DEBUG: inbox bookmark Add missing statuses un url') - return + return False if debug: print('DEBUG: c2s inbox bookmark Add request arrived in outbox') @@ -1162,59 +1158,55 @@ def _receiveUndoBookmark(recentPostsCache: {}, """Receives an undo bookmark activity within the POST section of HTTPServer """ if not messageJson.get('type'): - if debug: - print('DEBUG: inbox undo bookmark - no type') - return - if not messageJson['type'] == 'Remove': - if debug: - print('DEBUG: not a inbox undo bookmark Remove') - return + return False + if messageJson['type'] != 'Remove': + return False if not messageJson.get('actor'): if debug: print('DEBUG: no actor in inbox undo bookmark Remove') - return + return False if not messageJson.get('object'): if debug: print('DEBUG: no object in inbox undo bookmark Remove') - return + return False if not messageJson.get('target'): if debug: print('DEBUG: no target in inbox undo bookmark Remove') - return + return False if not isinstance(messageJson['object'], str): if debug: print('DEBUG: inbox undo bookmark Remove object is not string') - return + return False if not isinstance(messageJson['target'], str): if debug: print('DEBUG: inbox undo bookmark Remove target is not string') - return + return False domainFull = getFullDomain(domain, port) nickname = handle.split('@')[0] if not messageJson['actor'].endswith(domainFull + '/users/' + nickname): if debug: print('DEBUG: inbox undo bookmark Remove unexpected actor') - return + return False if not messageJson['target'].endswith(messageJson['actor'] + '/tlbookmarks'): if debug: print('DEBUG: inbox undo bookmark Remove target invalid ' + messageJson['target']) - return + return False if messageJson['object']['type'] != 'Document': if debug: print('DEBUG: inbox undo bookmark Remove type is not Document') - return + return False if not messageJson['object'].get('url'): if debug: print('DEBUG: inbox undo bookmark Remove missing url') - return + return False if '/statuses/' not in messageJson['object']['url']: if debug: print('DEBUG: inbox undo bookmark Remove missing statuses un url') - return + return False if debug: - print('DEBUG: c2s inbox undo bookmark Remove ' + + print('DEBUG: c2s inbox Remove bookmark ' + 'request arrived in outbox') messageUrl = removeIdEnding(messageJson['object']['url']) From 247bb06ba5a00ddb66f7fbace7053e456e5e8862 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 20 Mar 2021 14:25:24 +0000 Subject: [PATCH 61/87] Fix bookmark validation --- bookmarks.py | 16 ++++++++++++---- inbox.py | 16 ++++++++++++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/bookmarks.py b/bookmarks.py index f696c1d7c..7e178d7a6 100644 --- a/bookmarks.py +++ b/bookmarks.py @@ -534,9 +534,13 @@ def outboxBookmark(recentPostsCache: {}, if debug: print('DEBUG: no target in bookmark Add') return - if not isinstance(messageJson['object'], str): + if not isinstance(messageJson['object'], dict): if debug: - print('DEBUG: bookmark Add object is not string') + print('DEBUG: bookmark Add object is not dict') + return + if not messageJson['object'].get('type'): + if debug: + print('DEBUG: no object type in bookmark Add') return if not isinstance(messageJson['target'], str): if debug: @@ -599,9 +603,13 @@ def outboxUndoBookmark(recentPostsCache: {}, if debug: print('DEBUG: no target in unbookmark Remove') return - if not isinstance(messageJson['object'], str): + if not isinstance(messageJson['object'], dict): if debug: - print('DEBUG: unbookmark Remove object is not string') + print('DEBUG: unbookmark Remove object is not dict') + return + if not messageJson['object'].get('type'): + if debug: + print('DEBUG: no object type in bookmark Remove') return if not isinstance(messageJson['target'], str): if debug: diff --git a/inbox.py b/inbox.py index 84241a80b..bef4561d5 100644 --- a/inbox.py +++ b/inbox.py @@ -1098,10 +1098,14 @@ def _receiveBookmark(recentPostsCache: {}, if debug: print('DEBUG: no target in inbox bookmark Add') return False - if not isinstance(messageJson['object'], str): + if not isinstance(messageJson['object'], dict): if debug: print('DEBUG: inbox bookmark Add object is not string') return False + if not messageJson['object'].get('type'): + if debug: + print('DEBUG: no object type in inbox bookmark Add') + return False if not isinstance(messageJson['target'], str): if debug: print('DEBUG: inbox bookmark Add target is not string') @@ -1173,13 +1177,17 @@ def _receiveUndoBookmark(recentPostsCache: {}, if debug: print('DEBUG: no target in inbox undo bookmark Remove') return False - if not isinstance(messageJson['object'], str): + if not isinstance(messageJson['object'], dict): if debug: - print('DEBUG: inbox undo bookmark Remove object is not string') + print('DEBUG: inbox Remove bookmark object is not dict') + return False + if not messageJson['object'].get('type'): + if debug: + print('DEBUG: no object type in inbox bookmark Remove') return False if not isinstance(messageJson['target'], str): if debug: - print('DEBUG: inbox undo bookmark Remove target is not string') + print('DEBUG: inbox Remove bookmark target is not string') return False domainFull = getFullDomain(domain, port) nickname = handle.split('@')[0] From 2f4aa7bb3ef73703378178706a73102da2b395d1 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 20 Mar 2021 14:32:25 +0000 Subject: [PATCH 62/87] Add to field for bookmark remove --- bookmarks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bookmarks.py b/bookmarks.py index 7e178d7a6..d24a0150d 100644 --- a/bookmarks.py +++ b/bookmarks.py @@ -450,9 +450,11 @@ def sendUndoBookmarkViaServer(baseDir: str, session, "@context": "https://www.w3.org/ns/activitystreams", "type": "Remove", "actor": actor, + "to": [actor], "object": { "type": "Document", - "url": bookmarkUrl + "url": bookmarkUrl, + "to": [actor] }, "target": actor + "/tlbookmarks" } From c41e29c6996815fe3f18144a80aebcc2e8803340 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 20 Mar 2021 14:46:24 +0000 Subject: [PATCH 63/87] undo commands --- desktop_client.py | 71 ++++++++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index d8ede1975..5c01ce2a3 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -1184,6 +1184,46 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, cachedWebfingers, personCache, False, __version__) print('') + elif (commandStr == 'undo bookmark' or + commandStr == 'remove bookmark' or + commandStr == 'rm bookmark' or + commandStr == 'undo bm' or + commandStr == 'rm bm' or + commandStr == 'remove bm' or + commandStr == 'unbookmark' or + commandStr == 'bookmark undo' or + commandStr == 'bm undo ' or + commandStr.startswith('undo bm ') or + commandStr.startswith('remove bm ') or + commandStr.startswith('undo bookmark ') or + commandStr.startswith('remove bookmark ') or + commandStr.startswith('unbookmark ') or + commandStr.startswith('unbm ')): + currIndex = 0 + if ' ' in commandStr: + postIndex = commandStr.split(' ')[-1].strip() + if postIndex.isdigit(): + currIndex = int(postIndex) + if currIndex > 0 and boxJson: + postJsonObject = \ + _desktopGetBoxPostObject(boxJson, currIndex) + if postJsonObject: + if postJsonObject.get('id'): + likeActor = postJsonObject['object']['attributedTo'] + sayStr = 'Unbookmarking post by ' + \ + getNicknameFromActor(likeActor) + _sayCommand(sayStr, sayStr, + screenreader, + systemLanguage, espeak) + sessionLike = createSession(proxyType) + sendUndoBookmarkViaServer(baseDir, sessionLike, + nickname, password, + domain, port, httpPrefix, + postJsonObject['id'], + cachedWebfingers, + personCache, + False, __version__) + print('') elif (commandStr == 'bookmark' or commandStr == 'bm' or commandStr.startswith('bookmark ') or @@ -1212,37 +1252,6 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, cachedWebfingers, personCache, False, __version__) print('') - elif (commandStr == 'undo bookmark' or - commandStr == 'undo bm' or - commandStr == 'unbookmark' or - commandStr == 'unbm' or - commandStr.startswith('unbookmark ') or - commandStr.startswith('unbm ')): - currIndex = 0 - if ' ' in commandStr: - postIndex = commandStr.split(' ')[-1].strip() - if postIndex.isdigit(): - currIndex = int(postIndex) - if currIndex > 0 and boxJson: - postJsonObject = \ - _desktopGetBoxPostObject(boxJson, currIndex) - if postJsonObject: - if postJsonObject.get('id'): - likeActor = postJsonObject['object']['attributedTo'] - sayStr = 'Unbookmarking post by ' + \ - getNicknameFromActor(likeActor) - _sayCommand(sayStr, sayStr, - screenreader, - systemLanguage, espeak) - sessionLike = createSession(proxyType) - sendUndoBookmarkViaServer(baseDir, sessionLike, - nickname, password, - domain, port, httpPrefix, - postJsonObject['id'], - cachedWebfingers, - personCache, - False, __version__) - print('') elif commandStr == 'unlike' or commandStr == 'undo like': currIndex = 0 if ' ' in commandStr: From c13d4f6e93288d1e5877b5140db610199cd63718 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 20 Mar 2021 18:10:08 +0000 Subject: [PATCH 64/87] Show content warnings in desktop client --- desktop_client.py | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index 5c01ce2a3..645c406bf 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -262,6 +262,7 @@ def _desktopReplyToPost(session, postId: str, sayStr = 'No reply was entered.' _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) return + print('') sayStr = 'You entered this reply:' _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) _sayCommand(replyMessage, replyMessage, screenreader, @@ -651,6 +652,11 @@ def _desktopShowBox(boxName: str, boxJson: {}, posStr = _padToWidth(ctrStr, numberWidth) authorActor = postJsonObject['object']['attributedTo'] + contentWarning = None + if postJsonObject['object'].get('summary'): + contentWarning = '⚡' + \ + _padToWidth(postJsonObject['object']['summary'], + contentWidth) name = getNicknameFromActor(authorActor) # append icons to the end of the name @@ -660,12 +666,6 @@ def _desktopShowBox(boxName: str, boxJson: {}, spaceAdded = True name += ' ' name += '↲' - if boxName != 'dm': - if isDM(postJsonObject): - if not spaceAdded: - spaceAdded = True - name += ' ' - name += '📧' likesCount = noOfLikes(postJsonObject) if likesCount > 10: likesCount = 10 @@ -679,11 +679,24 @@ def _desktopShowBox(boxName: str, boxJson: {}, published = _formatPublished(postJsonObject['published']) content = _textOnlyContent(postJsonObject['object']['content']) - if isPGPEncrypted(content): - content = '🔒' + content - elif '://' in content: - content = '🔗' + content - content = _padToWidth(content, contentWidth) + if boxName != 'dm': + if isDM(postJsonObject): + content = '📧' + content + if not contentWarning: + if isPGPEncrypted(content): + content = '🔒' + content + elif '://' in content: + content = '🔗' + content + content = _padToWidth(content, contentWidth) + else: + # display content warning + if isPGPEncrypted(content): + content = '🔒' + contentWarning + else: + if '://' in content: + content = '🔗' + contentWarning + else: + content = contentWarning print(indent + str(posStr) + ' | ' + name + ' | ' + published + ' | ' + content) ctr += 1 From 44df9eabafdf80fe3543f04db5df321f38bc0e28 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 20 Mar 2021 21:20:41 +0000 Subject: [PATCH 65/87] Mute and unmute via c2s --- blocking.py | 200 ++++++++++++++++++++++++++++++++++++++++++ daemon.py | 4 +- epicyon.py | 60 +++++++++++++ outbox.py | 18 ++++ posts.py | 247 ++++++++++++++++++++++++++++++++-------------------- 5 files changed, 433 insertions(+), 96 deletions(-) diff --git a/blocking.py b/blocking.py index 29aa2a45b..f05bfd1be 100644 --- a/blocking.py +++ b/blocking.py @@ -7,7 +7,10 @@ __email__ = "bob@freedombone.net" __status__ = "Production" import os +import json from datetime import datetime +from utils import getCachedPostFilename +from utils import loadJson from utils import fileLastModified from utils import setConfigParam from utils import hasUsersPath @@ -361,6 +364,203 @@ def outboxUndoBlock(baseDir: str, httpPrefix: str, print('DEBUG: post undo blocked via c2s - ' + postFilename) +def mutePost(baseDir: str, nickname: str, domain: str, postId: str, + recentPostsCache: {}) -> None: + """ Mutes the given post + """ + postFilename = locatePost(baseDir, nickname, domain, postId) + if not postFilename: + return + postJsonObject = loadJson(postFilename) + if not postJsonObject: + return + + # remove cached post so that the muted version gets recreated + # without its content text and/or image + cachedPostFilename = \ + getCachedPostFilename(baseDir, nickname, domain, postJsonObject) + if cachedPostFilename: + if os.path.isfile(cachedPostFilename): + os.remove(cachedPostFilename) + + muteFile = open(postFilename + '.muted', 'w+') + if muteFile: + muteFile.write('\n') + muteFile.close() + print('MUTE: ' + postFilename + '.muted file added') + + # if the post is in the recent posts cache then mark it as muted + if recentPostsCache.get('index'): + postId = \ + removeIdEnding(postJsonObject['id']).replace('/', '#') + if postId in recentPostsCache['index']: + print('MUTE: ' + postId + ' is in recent posts cache') + if recentPostsCache['json'].get(postId): + postJsonObject['muted'] = True + recentPostsCache['json'][postId] = json.dumps(postJsonObject) + if recentPostsCache.get('html'): + if recentPostsCache['html'].get(postId): + del recentPostsCache['html'][postId] + print('MUTE: ' + postId + + ' marked as muted in recent posts memory cache') + + +def unmutePost(baseDir: str, nickname: str, domain: str, postId: str, + recentPostsCache: {}) -> None: + """ Unmutes the given post + """ + postFilename = locatePost(baseDir, nickname, domain, postId) + if not postFilename: + return + postJsonObject = loadJson(postFilename) + if not postJsonObject: + return + + muteFilename = postFilename + '.muted' + if os.path.isfile(muteFilename): + os.remove(muteFilename) + print('UNMUTE: ' + muteFilename + ' file removed') + + # remove cached post so that the muted version gets recreated + # with its content text and/or image + cachedPostFilename = \ + getCachedPostFilename(baseDir, nickname, domain, postJsonObject) + if cachedPostFilename: + if os.path.isfile(cachedPostFilename): + os.remove(cachedPostFilename) + + # if the post is in the recent posts cache then mark it as unmuted + if recentPostsCache.get('index'): + postId = \ + removeIdEnding(postJsonObject['id']).replace('/', '#') + if postId in recentPostsCache['index']: + print('UNMUTE: ' + postId + ' is in recent posts cache') + if recentPostsCache['json'].get(postId): + postJsonObject['muted'] = False + recentPostsCache['json'][postId] = json.dumps(postJsonObject) + if recentPostsCache.get('html'): + if recentPostsCache['html'].get(postId): + del recentPostsCache['html'][postId] + print('UNMUTE: ' + postId + + ' marked as unmuted in recent posts cache') + + +def outboxMute(baseDir: str, httpPrefix: str, + nickname: str, domain: str, port: int, + messageJson: {}, debug: bool, + recentPostsCache: {}) -> None: + """When a mute is received by the outbox from c2s + """ + if not messageJson.get('type'): + return + if not messageJson.get('actor'): + return + domainFull = getFullDomain(domain, port) + if not messageJson['actor'].endswith(domainFull + '/users/' + nickname): + return + if not messageJson['type'] == 'Ignore': + return + if not messageJson.get('object'): + if debug: + print('DEBUG: no object in mute') + return + if not isinstance(messageJson['object'], str): + if debug: + print('DEBUG: mute object is not string') + return + if debug: + print('DEBUG: c2s mute request arrived in outbox') + + messageId = removeIdEnding(messageJson['object']) + if '/statuses/' not in messageId: + if debug: + print('DEBUG: c2s mute object is not a status') + return + if not hasUsersPath(messageId): + if debug: + print('DEBUG: c2s mute object has no nickname') + return + if ':' in domain: + domain = domain.split(':')[0] + postFilename = locatePost(baseDir, nickname, domain, messageId) + if not postFilename: + if debug: + print('DEBUG: c2s mute post not found in inbox or outbox') + print(messageId) + return + nicknameMuted = getNicknameFromActor(messageJson['object']) + if not nicknameMuted: + print('WARN: unable to find nickname in ' + messageJson['object']) + return + + mutePost(baseDir, nickname, domain, + messageJson['object'], recentPostsCache) + + if debug: + print('DEBUG: post muted via c2s - ' + postFilename) + + +def outboxUndoMute(baseDir: str, httpPrefix: str, + nickname: str, domain: str, port: int, + messageJson: {}, debug: bool, + recentPostsCache: {}) -> None: + """When an undo mute is received by the outbox from c2s + """ + if not messageJson.get('type'): + return + if not messageJson.get('actor'): + return + domainFull = getFullDomain(domain, port) + if not messageJson['actor'].endswith(domainFull + '/users/' + nickname): + return + if not messageJson['type'] == 'Undo': + return + if not messageJson.get('object'): + return + if not isinstance(messageJson['object'], dict): + return + if not messageJson['object'].get('type'): + return + if messageJson['object']['type'] != 'Ignore': + return + if not isinstance(messageJson['object']['object'], str): + if debug: + print('DEBUG: undo mute object is not a string') + return + if debug: + print('DEBUG: c2s undo mute request arrived in outbox') + + messageId = removeIdEnding(messageJson['object']['object']) + if '/statuses/' not in messageId: + if debug: + print('DEBUG: c2s undo mute object is not a status') + return + if not hasUsersPath(messageId): + if debug: + print('DEBUG: c2s undo mute object has no nickname') + return + if ':' in domain: + domain = domain.split(':')[0] + postFilename = locatePost(baseDir, nickname, domain, messageId) + if not postFilename: + if debug: + print('DEBUG: c2s undo mute post not found in inbox or outbox') + print(messageId) + return + nicknameMuted = getNicknameFromActor(messageJson['object']['object']) + if not nicknameMuted: + print('WARN: unable to find nickname in ' + + messageJson['object']['object']) + return + + unmutePost(baseDir, nickname, domain, + messageJson['object']['object'], + recentPostsCache) + + if debug: + print('DEBUG: post undo mute via c2s - ' + postFilename) + + def setBrochMode(baseDir: str, domainFull: str, enabled: bool) -> None: """Broch mode can be used to lock down the instance during a period of time when it is temporarily under attack. diff --git a/daemon.py b/daemon.py index ee93178ff..4825db70a 100644 --- a/daemon.py +++ b/daemon.py @@ -73,8 +73,6 @@ from posts import pinPost from posts import jsonPinPost from posts import undoPinnedPost from posts import isModerator -from posts import mutePost -from posts import unmutePost from posts import createQuestionPost from posts import createPublicPost from posts import createBlogPost @@ -108,6 +106,8 @@ from threads import threadWithTrace from threads import removeDormantThreads from media import replaceYouTube from media import attachMedia +from blocking import mutePost +from blocking import unmutePost from blocking import setBrochMode from blocking import addBlock from blocking import removeBlock diff --git a/epicyon.py b/epicyon.py index 5b2d3fc97..b5575c1a4 100644 --- a/epicyon.py +++ b/epicyon.py @@ -24,6 +24,8 @@ from roles import setRole from webfinger import webfingerHandle from bookmarks import sendBookmarkViaServer from bookmarks import sendUndoBookmarkViaServer +from posts import sendMuteViaServer +from posts import sendUndoMuteViaServer from posts import c2sBoxJson from posts import downloadFollowCollection from posts import getPublicPostDomains @@ -478,6 +480,10 @@ parser.add_argument('--block', dest='block', type=str, default=None, help='Block a particular address') parser.add_argument('--unblock', dest='unblock', type=str, default=None, help='Remove a block on a particular address') +parser.add_argument('--mute', dest='mute', type=str, default=None, + help='Mute a particular post URL') +parser.add_argument('--unmute', dest='unmute', type=str, default=None, + help='Unmute a particular post URL') parser.add_argument('--delegate', dest='delegate', type=str, default=None, help='Address of an account to delegate a role to') parser.add_argument('--undodelegate', '--undelegate', dest='undelegate', @@ -2036,6 +2042,60 @@ if args.block: time.sleep(1) sys.exit() +if args.mute: + if not nickname: + print('Specify a nickname with the --nickname option') + sys.exit() + + if not args.password: + args.password = getpass.getpass('Password: ') + if not args.password: + print('Specify a password with the --password option') + sys.exit() + args.password = args.password.replace('\n', '') + + session = createSession(proxyType) + personCache = {} + cachedWebfingers = {} + print('Sending mute of ' + args.mute) + + sendMuteViaServer(baseDir, session, nickname, args.password, + domain, port, + httpPrefix, args.mute, + cachedWebfingers, personCache, + True, __version__) + for i in range(10): + # TODO detect send success/fail + time.sleep(1) + sys.exit() + +if args.unmute: + if not nickname: + print('Specify a nickname with the --nickname option') + sys.exit() + + if not args.password: + args.password = getpass.getpass('Password: ') + if not args.password: + print('Specify a password with the --password option') + sys.exit() + args.password = args.password.replace('\n', '') + + session = createSession(proxyType) + personCache = {} + cachedWebfingers = {} + print('Sending undo mute of ' + args.unmute) + + sendUndoMuteViaServer(baseDir, session, nickname, args.password, + domain, port, + httpPrefix, args.unmute, + cachedWebfingers, personCache, + True, __version__) + for i in range(10): + # TODO detect send success/fail + time.sleep(1) + sys.exit() + if args.delegate: if not nickname: print('Specify a nickname with the --nickname option') diff --git a/outbox.py b/outbox.py index 0b04cdf82..8940cd691 100644 --- a/outbox.py +++ b/outbox.py @@ -26,6 +26,8 @@ from utils import saveJson from blocking import isBlockedDomain from blocking import outboxBlock from blocking import outboxUndoBlock +from blocking import outboxMute +from blocking import outboxUndoMute from media import replaceYouTube from media import getMediaPath from media import createMediaDirs @@ -515,6 +517,22 @@ def postMessageToOutbox(session, translate: {}, postToNickname, domain, port, messageJson, debug) + if debug: + print('DEBUG: handle mute requests') + outboxMute(baseDir, httpPrefix, + postToNickname, domain, + port, + messageJson, debug, + recentPostsCache) + + if debug: + print('DEBUG: handle undo mute requests') + outboxUndoMute(baseDir, httpPrefix, + postToNickname, domain, + port, + messageJson, debug, + recentPostsCache) + if debug: print('DEBUG: handle share uploads') outboxShareUpload(baseDir, httpPrefix, diff --git a/posts.py b/posts.py index 5d5a6e9e2..80c2a5c0a 100644 --- a/posts.py +++ b/posts.py @@ -40,8 +40,6 @@ from utils import validPostDate from utils import getFullDomain from utils import getFollowersList from utils import isEvil -from utils import removeIdEnding -from utils import getCachedPostFilename from utils import getStatusNumber from utils import createPersonDir from utils import urlPermitted @@ -1827,17 +1825,6 @@ def createReportPost(baseDir: str, if not postJsonObject: continue - # update the inbox index with the report filename - # indexFilename = baseDir+'/accounts/'+handle+'/inbox.index' - # indexEntry = \ - # removeIdEnding(postJsonObject['id']).replace('/','#') + '.json' - # if indexEntry not in open(indexFilename).read(): - # try: - # with open(indexFilename, 'a+') as fp: - # fp.write(indexEntry) - # except: - # pass - # save a notification file so that the moderator # knows something new has appeared newReportFile = baseDir + '/accounts/' + handle + '/.newReport' @@ -4065,87 +4052,6 @@ def isMuted(baseDir: str, nickname: str, domain: str, postId: str) -> bool: return False -def mutePost(baseDir: str, nickname: str, domain: str, postId: str, - recentPostsCache: {}) -> None: - """ Mutes the given post - """ - postFilename = locatePost(baseDir, nickname, domain, postId) - if not postFilename: - return - postJsonObject = loadJson(postFilename) - if not postJsonObject: - return - - # remove cached post so that the muted version gets recreated - # without its content text and/or image - cachedPostFilename = \ - getCachedPostFilename(baseDir, nickname, domain, postJsonObject) - if cachedPostFilename: - if os.path.isfile(cachedPostFilename): - os.remove(cachedPostFilename) - - muteFile = open(postFilename + '.muted', 'w+') - if muteFile: - muteFile.write('\n') - muteFile.close() - print('MUTE: ' + postFilename + '.muted file added') - - # if the post is in the recent posts cache then mark it as muted - if recentPostsCache.get('index'): - postId = \ - removeIdEnding(postJsonObject['id']).replace('/', '#') - if postId in recentPostsCache['index']: - print('MUTE: ' + postId + ' is in recent posts cache') - if recentPostsCache['json'].get(postId): - postJsonObject['muted'] = True - recentPostsCache['json'][postId] = json.dumps(postJsonObject) - if recentPostsCache.get('html'): - if recentPostsCache['html'].get(postId): - del recentPostsCache['html'][postId] - print('MUTE: ' + postId + - ' marked as muted in recent posts memory cache') - - -def unmutePost(baseDir: str, nickname: str, domain: str, postId: str, - recentPostsCache: {}) -> None: - """ Unmutes the given post - """ - postFilename = locatePost(baseDir, nickname, domain, postId) - if not postFilename: - return - postJsonObject = loadJson(postFilename) - if not postJsonObject: - return - - muteFilename = postFilename + '.muted' - if os.path.isfile(muteFilename): - os.remove(muteFilename) - print('UNMUTE: ' + muteFilename + ' file removed') - - # remove cached post so that the muted version gets recreated - # with its content text and/or image - cachedPostFilename = \ - getCachedPostFilename(baseDir, nickname, domain, postJsonObject) - if cachedPostFilename: - if os.path.isfile(cachedPostFilename): - os.remove(cachedPostFilename) - - # if the post is in the recent posts cache then mark it as unmuted - if recentPostsCache.get('index'): - postId = \ - removeIdEnding(postJsonObject['id']).replace('/', '#') - if postId in recentPostsCache['index']: - print('UNMUTE: ' + postId + ' is in recent posts cache') - if recentPostsCache['json'].get(postId): - postJsonObject['muted'] = False - recentPostsCache['json'][postId] = json.dumps(postJsonObject) - if recentPostsCache.get('html'): - if recentPostsCache['html'].get(postId): - del recentPostsCache['html'][postId] - print('UNMUTE: ' + postId + - ' marked as unmuted in recent posts cache') - - def sendBlockViaServer(baseDir: str, session, fromNickname: str, password: str, fromDomain: str, fromPort: int, @@ -4226,6 +4132,159 @@ def sendBlockViaServer(baseDir: str, session, return newBlockJson +def sendMuteViaServer(baseDir: str, session, + fromNickname: str, password: str, + fromDomain: str, fromPort: int, + httpPrefix: str, mutedUrl: str, + cachedWebfingers: {}, personCache: {}, + debug: bool, projectVersion: str) -> {}: + """Creates a mute via c2s + """ + if not session: + print('WARN: No session for sendMuteViaServer') + return 6 + + fromDomainFull = getFullDomain(fromDomain, fromPort) + + actor = httpPrefix + '://' + fromDomainFull + '/users/' + fromNickname + handle = actor.replace('/users/', '/@') + + newMuteJson = { + "@context": "https://www.w3.org/ns/activitystreams", + 'type': 'Ignore', + 'actor': actor, + 'object': mutedUrl + } + + # lookup the inbox for the To handle + wfRequest = webfingerHandle(session, handle, httpPrefix, + cachedWebfingers, + fromDomain, projectVersion, debug) + if not wfRequest: + if debug: + print('DEBUG: mute webfinger failed for ' + handle) + return 1 + if not isinstance(wfRequest, dict): + print('WARN: mute Webfinger for ' + handle + + ' did not return a dict. ' + str(wfRequest)) + return 1 + + postToBox = 'outbox' + + # get the actor inbox for the To handle + (inboxUrl, pubKeyId, pubKey, + fromPersonId, sharedInbox, avatarUrl, + displayName) = getPersonBox(baseDir, session, wfRequest, + personCache, + projectVersion, httpPrefix, fromNickname, + fromDomain, postToBox, 72652) + + if not inboxUrl: + if debug: + print('DEBUG: mute no ' + postToBox + ' was found for ' + handle) + return 3 + if not fromPersonId: + if debug: + print('DEBUG: mute no actor was found for ' + handle) + return 4 + + authHeader = createBasicAuthHeader(fromNickname, password) + + headers = { + 'host': fromDomain, + 'Content-type': 'application/json', + 'Authorization': authHeader + } + postResult = postJson(session, newMuteJson, [], inboxUrl, + headers, 30, True) + if not postResult: + print('WARN: mute unable to post') + + if debug: + print('DEBUG: c2s POST mute success') + + return newMuteJson + + +def sendUndoMuteViaServer(baseDir: str, session, + fromNickname: str, password: str, + fromDomain: str, fromPort: int, + httpPrefix: str, mutedUrl: str, + cachedWebfingers: {}, personCache: {}, + debug: bool, projectVersion: str) -> {}: + """Undoes a mute via c2s + """ + if not session: + print('WARN: No session for sendUndoMuteViaServer') + return 6 + + fromDomainFull = getFullDomain(fromDomain, fromPort) + + actor = httpPrefix + '://' + fromDomainFull + '/users/' + fromNickname + handle = actor.replace('/users/', '/@') + + undoMuteJson = { + "@context": "https://www.w3.org/ns/activitystreams", + 'type': 'Undo', + 'actor': actor, + 'object': { + 'type': 'Ignore', + 'actor': actor, + 'object': mutedUrl + } + } + + # lookup the inbox for the To handle + wfRequest = webfingerHandle(session, handle, httpPrefix, + cachedWebfingers, + fromDomain, projectVersion, debug) + if not wfRequest: + if debug: + print('DEBUG: undo mute webfinger failed for ' + handle) + return 1 + if not isinstance(wfRequest, dict): + print('WARN: undo mute Webfinger for ' + handle + + ' did not return a dict. ' + str(wfRequest)) + return 1 + + postToBox = 'outbox' + + # get the actor inbox for the To handle + (inboxUrl, pubKeyId, pubKey, + fromPersonId, sharedInbox, avatarUrl, + displayName) = getPersonBox(baseDir, session, wfRequest, + personCache, + projectVersion, httpPrefix, fromNickname, + fromDomain, postToBox, 72652) + + if not inboxUrl: + if debug: + print('DEBUG: undo mute no ' + postToBox + + ' was found for ' + handle) + return 3 + if not fromPersonId: + if debug: + print('DEBUG: undo mute no actor was found for ' + handle) + return 4 + + authHeader = createBasicAuthHeader(fromNickname, password) + + headers = { + 'host': fromDomain, + 'Content-type': 'application/json', + 'Authorization': authHeader + } + postResult = postJson(session, undoMuteJson, [], inboxUrl, + headers, 30, True) + if not postResult: + print('WARN: undo mute unable to post') + + if debug: + print('DEBUG: c2s POST undo mute success') + + return undoMuteJson + + def sendUndoBlockViaServer(baseDir: str, session, fromNickname: str, password: str, fromDomain: str, fromPort: int, From d98dc388d8de65fc00795177c5b6f6ab82ed82d9 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 21 Mar 2021 10:45:24 +0000 Subject: [PATCH 66/87] Add ignores collection to make mutes visible to c2s --- blocking.py | 84 ++++++++++++++++++++++++++++++++++++++++++++++------ bookmarks.py | 16 +++++----- daemon.py | 14 ++++++--- posts.py | 2 ++ utils.py | 3 +- 5 files changed, 97 insertions(+), 22 deletions(-) diff --git a/blocking.py b/blocking.py index f05bfd1be..ff59f2d4b 100644 --- a/blocking.py +++ b/blocking.py @@ -11,6 +11,7 @@ import json from datetime import datetime from utils import getCachedPostFilename from utils import loadJson +from utils import saveJson from utils import fileLastModified from utils import setConfigParam from utils import hasUsersPath @@ -364,8 +365,9 @@ def outboxUndoBlock(baseDir: str, httpPrefix: str, print('DEBUG: post undo blocked via c2s - ' + postFilename) -def mutePost(baseDir: str, nickname: str, domain: str, postId: str, - recentPostsCache: {}) -> None: +def mutePost(baseDir: str, nickname: str, domain: str, port: int, + httpPrefix: str, postId: str, recentPostsCache: {}, + debug: bool) -> None: """ Mutes the given post """ postFilename = locatePost(baseDir, nickname, domain, postId) @@ -375,6 +377,42 @@ def mutePost(baseDir: str, nickname: str, domain: str, postId: str, if not postJsonObject: return + if postJsonObject.get('object'): + if isinstance(postJsonObject['object'], dict): + domainFull = getFullDomain(domain, port) + actor = httpPrefix + '://' + domainFull + '/users/' + nickname + # does this post have ignores on it from differenent actors? + if not postJsonObject['object'].get('ignores'): + if debug: + print('DEBUG: Adding initial mute to ' + postId) + ignoresJson = { + "@context": "https://www.w3.org/ns/activitystreams", + 'id': postId, + 'type': 'Collection', + "totalItems": 1, + 'items': [{ + 'type': 'Ignore', + 'actor': actor + }] + } + postJsonObject['object']['ignores'] = ignoresJson + else: + if not postJsonObject['object']['ignores'].get('items'): + postJsonObject['object']['ignores']['items'] = [] + itemsList = postJsonObject['object']['ignores']['items'] + for ignoresItem in itemsList: + if ignoresItem.get('actor'): + if ignoresItem['actor'] == actor: + return + newIgnore = { + 'type': 'Ignore', + 'actor': actor + } + igIt = len(itemsList) + itemsList.append(newIgnore) + postJsonObject['object']['ignores']['totalItems'] = igIt + saveJson(postJsonObject, postFilename) + # remove cached post so that the muted version gets recreated # without its content text and/or image cachedPostFilename = \ @@ -405,8 +443,9 @@ def mutePost(baseDir: str, nickname: str, domain: str, postId: str, ' marked as muted in recent posts memory cache') -def unmutePost(baseDir: str, nickname: str, domain: str, postId: str, - recentPostsCache: {}) -> None: +def unmutePost(baseDir: str, nickname: str, domain: str, port: int, + httpPrefix: str, postId: str, recentPostsCache: {}, + debug: bool) -> None: """ Unmutes the given post """ postFilename = locatePost(baseDir, nickname, domain, postId) @@ -421,6 +460,32 @@ def unmutePost(baseDir: str, nickname: str, domain: str, postId: str, os.remove(muteFilename) print('UNMUTE: ' + muteFilename + ' file removed') + if postJsonObject.get('object'): + if isinstance(postJsonObject['object'], dict): + if postJsonObject['object'].get('ignores'): + domainFull = getFullDomain(domain, port) + actor = httpPrefix + '://' + domainFull + '/users/' + nickname + totalItems = 0 + if postJsonObject['object']['ignores'].get('totalItems'): + totalItems = \ + postJsonObject['object']['ignores']['totalItems'] + itemsList = postJsonObject['object']['ignores']['items'] + for ignoresItem in itemsList: + if ignoresItem.get('actor'): + if ignoresItem['actor'] == actor: + if debug: + print('DEBUG: mute was removed for ' + actor) + itemsList.remove(ignoresItem) + break + if totalItems == 1: + if debug: + print('DEBUG: mute was removed from post') + del postJsonObject['object']['ignores'] + else: + igItLen = len(postJsonObject['object']['ignores']['items']) + postJsonObject['object']['ignores']['totalItems'] = igItLen + saveJson(postJsonObject, postFilename) + # remove cached post so that the muted version gets recreated # with its content text and/or image cachedPostFilename = \ @@ -493,8 +558,9 @@ def outboxMute(baseDir: str, httpPrefix: str, print('WARN: unable to find nickname in ' + messageJson['object']) return - mutePost(baseDir, nickname, domain, - messageJson['object'], recentPostsCache) + mutePost(baseDir, nickname, domain, port, + httpPrefix, messageJson['object'], recentPostsCache, + debug) if debug: print('DEBUG: post muted via c2s - ' + postFilename) @@ -553,9 +619,9 @@ def outboxUndoMute(baseDir: str, httpPrefix: str, messageJson['object']['object']) return - unmutePost(baseDir, nickname, domain, - messageJson['object']['object'], - recentPostsCache) + unmutePost(baseDir, nickname, domain, port, + httpPrefix, messageJson['object']['object'], + recentPostsCache, debug) if debug: print('DEBUG: post undo mute via c2s - ' + postFilename) diff --git a/bookmarks.py b/bookmarks.py index d24a0150d..f69d4c7bc 100644 --- a/bookmarks.py +++ b/bookmarks.py @@ -185,14 +185,14 @@ def updateBookmarksCollection(recentPostsCache: {}, if bookmarkItem.get('actor'): if bookmarkItem['actor'] == actor: return - newBookmark = { - 'type': 'Bookmark', - 'actor': actor - } - nb = newBookmark - bmIt = len(postJsonObject['object']['bookmarks']['items']) - postJsonObject['object']['bookmarks']['items'].append(nb) - postJsonObject['object']['bookmarks']['totalItems'] = bmIt + newBookmark = { + 'type': 'Bookmark', + 'actor': actor + } + nb = newBookmark + bmIt = len(postJsonObject['object']['bookmarks']['items']) + postJsonObject['object']['bookmarks']['items'].append(nb) + postJsonObject['object']['bookmarks']['totalItems'] = bmIt if debug: print('DEBUG: saving post with bookmarks added') diff --git a/daemon.py b/daemon.py index 4825db70a..f87b7a830 100644 --- a/daemon.py +++ b/daemon.py @@ -471,6 +471,8 @@ class PubServer(BaseHTTPRequestHandler): postJsonObject['replies'] = {} if postJsonObject.get('bookmarks'): postJsonObject['bookmarks'] = {} + if postJsonObject.get('ignores'): + postJsonObject['ignores'] = {} if not postJsonObject.get('object'): return if not isinstance(postJsonObject['object'], dict): @@ -483,6 +485,8 @@ class PubServer(BaseHTTPRequestHandler): postJsonObject['object']['replies'] = {} if postJsonObject['object'].get('bookmarks'): postJsonObject['object']['bookmarks'] = {} + if postJsonObject['object'].get('ignores'): + postJsonObject['object']['ignores'] = {} def _requestHTTP(self) -> bool: """Should a http response be given? @@ -7010,8 +7014,9 @@ class PubServer(BaseHTTPRequestHandler): actor = \ httpPrefix + '://' + domainFull + path.split('?mute=')[0] nickname = getNicknameFromActor(actor) - mutePost(baseDir, nickname, domain, - muteUrl, self.server.recentPostsCache) + mutePost(baseDir, nickname, domain, port, + httpPrefix, muteUrl, + self.server.recentPostsCache, debug) self.server.GETbusy = False if callingDomain.endswith('.onion') and onionDomain: actor = \ @@ -7054,8 +7059,9 @@ class PubServer(BaseHTTPRequestHandler): actor = \ httpPrefix + '://' + domainFull + path.split('?unmute=')[0] nickname = getNicknameFromActor(actor) - unmutePost(baseDir, nickname, domain, - muteUrl, self.server.recentPostsCache) + unmutePost(baseDir, nickname, domain, port, + httpPrefix, muteUrl, + self.server.recentPostsCache, debug) self.server.GETbusy = False if callingDomain.endswith('.onion') and onionDomain: actor = \ diff --git a/posts.py b/posts.py index 80c2a5c0a..d82a068a1 100644 --- a/posts.py +++ b/posts.py @@ -3273,6 +3273,8 @@ def _createBoxIndexed(recentPostsCache: {}, p['shares'] = {} if p['object'].get('bookmarks'): p['bookmarks'] = {} + if p['object'].get('ignores'): + p['ignores'] = {} boxItems['orderedItems'].append(p) diff --git a/utils.py b/utils.py index 04bde5f5e..bf6a3c54b 100644 --- a/utils.py +++ b/utils.py @@ -1359,7 +1359,8 @@ def _isReservedName(nickname: str) -> bool: 'updates', 'repeat', 'announce', 'shares', 'fonts', 'icons', 'avatars', 'welcome', 'helpimages', - 'bookmark', 'bookmarks', 'tlbookmarks') + 'bookmark', 'bookmarks', 'tlbookmarks', + 'ignores') if nickname in reservedNames: return True return False From 4c2d4eb049ee2afe70e5e20dcd1c8a9561cf3fa6 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 21 Mar 2021 12:15:10 +0000 Subject: [PATCH 67/87] Show muted posts in desktop client --- desktop_client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/desktop_client.py b/desktop_client.py index 645c406bf..a65213fb6 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -697,6 +697,8 @@ def _desktopShowBox(boxName: str, boxJson: {}, content = '🔗' + contentWarning else: content = contentWarning + if postJsonObject['object'].get('ignores'): + content = '🔇' print(indent + str(posStr) + ' | ' + name + ' | ' + published + ' | ' + content) ctr += 1 From 93ece7036c99595b10e8fa49d9a66567f7f26c2a Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 21 Mar 2021 12:31:48 +0000 Subject: [PATCH 68/87] Mute/unmute commands --- README_commandline.md | 2 + desktop_client.py | 89 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 82 insertions(+), 9 deletions(-) diff --git a/README_commandline.md b/README_commandline.md index bd8be89f0..a8b1019c9 100644 --- a/README_commandline.md +++ b/README_commandline.md @@ -458,6 +458,8 @@ like Like the last post unlike Unlike the last post bookmark Bookmark the last post unbookmark Unbookmark the last post +mute Mute the last post +unmute Unmute the last post reply Reply to the last post post Create a new post post to [handle] Create a new direct message diff --git a/desktop_client.py b/desktop_client.py index a65213fb6..8163a1942 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -29,6 +29,8 @@ from like import sendLikeViaServer from like import sendUndoLikeViaServer from follow import sendFollowRequestViaServer from follow import sendUnfollowRequestViaServer +from posts import sendMuteViaServer +from posts import sendUndoMuteViaServer from posts import sendPostViaServer from posts import c2sBoxJson from posts import downloadAnnounce @@ -69,9 +71,13 @@ def _desktopHelp() -> None: print(indent + 'unlike ' + 'Unlike the last post') print(indent + 'bookmark ' + - 'bookmark the last post') + 'Bookmark the last post') print(indent + 'unbookmark ' + 'Unbookmark the last post') + print(indent + 'mute ' + + 'Mute the last post') + print(indent + 'unmute ' + + 'Unmute the last post') print(indent + 'reply ' + 'Reply to the last post') print(indent + 'post ' + @@ -1199,6 +1205,71 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, cachedWebfingers, personCache, False, __version__) print('') + elif (commandStr == 'undo mute' or + commandStr == 'undo ignore' or + commandStr == 'remove mute' or + commandStr == 'rm mute' or + commandStr == 'unmute' or + commandStr == 'unignore' or + commandStr == 'mute undo' or + commandStr.startswith('undo mute ') or + commandStr.startswith('undo ignore ') or + commandStr.startswith('remove mute ') or + commandStr.startswith('remove ignore ') or + commandStr.startswith('unignore ') or + commandStr.startswith('unmute ')): + currIndex = 0 + if ' ' in commandStr: + postIndex = commandStr.split(' ')[-1].strip() + if postIndex.isdigit(): + currIndex = int(postIndex) + if currIndex > 0 and boxJson: + postJsonObject = \ + _desktopGetBoxPostObject(boxJson, currIndex) + if postJsonObject: + if postJsonObject.get('id'): + muteActor = postJsonObject['object']['attributedTo'] + sayStr = 'Unmuting post by ' + \ + getNicknameFromActor(muteActor) + _sayCommand(sayStr, sayStr, + screenreader, + systemLanguage, espeak) + sessionMute = createSession(proxyType) + sendUndoMuteViaServer(baseDir, sessionMute, + nickname, password, + domain, port, + httpPrefix, postJsonObject['id'], + cachedWebfingers, personCache, + False, __version__) + print('') + elif (commandStr == 'mute' or + commandStr == 'ignore' or + commandStr.startswith('mute ') or + commandStr.startswith('ignore ')): + currIndex = 0 + if ' ' in commandStr: + postIndex = commandStr.split(' ')[-1].strip() + if postIndex.isdigit(): + currIndex = int(postIndex) + if currIndex > 0 and boxJson: + postJsonObject = \ + _desktopGetBoxPostObject(boxJson, currIndex) + if postJsonObject: + if postJsonObject.get('id'): + muteActor = postJsonObject['object']['attributedTo'] + sayStr = 'Muting post by ' + \ + getNicknameFromActor(muteActor) + _sayCommand(sayStr, sayStr, + screenreader, + systemLanguage, espeak) + sessionMute = createSession(proxyType) + sendMuteViaServer(baseDir, sessionMute, + nickname, password, + domain, port, + httpPrefix, postJsonObject['id'], + cachedWebfingers, personCache, + False, __version__) + print('') elif (commandStr == 'undo bookmark' or commandStr == 'remove bookmark' or commandStr == 'rm bookmark' or @@ -1224,14 +1295,14 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, _desktopGetBoxPostObject(boxJson, currIndex) if postJsonObject: if postJsonObject.get('id'): - likeActor = postJsonObject['object']['attributedTo'] + bmActor = postJsonObject['object']['attributedTo'] sayStr = 'Unbookmarking post by ' + \ - getNicknameFromActor(likeActor) + getNicknameFromActor(bmActor) _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) - sessionLike = createSession(proxyType) - sendUndoBookmarkViaServer(baseDir, sessionLike, + sessionbm = createSession(proxyType) + sendUndoBookmarkViaServer(baseDir, sessionbm, nickname, password, domain, port, httpPrefix, postJsonObject['id'], @@ -1253,14 +1324,14 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, _desktopGetBoxPostObject(boxJson, currIndex) if postJsonObject: if postJsonObject.get('id'): - likeActor = postJsonObject['object']['attributedTo'] + bmActor = postJsonObject['object']['attributedTo'] sayStr = 'Bookmarking post by ' + \ - getNicknameFromActor(likeActor) + getNicknameFromActor(bmActor) _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) - sessionLike = createSession(proxyType) - sendBookmarkViaServer(baseDir, sessionLike, + sessionbm = createSession(proxyType) + sendBookmarkViaServer(baseDir, sessionbm, nickname, password, domain, port, httpPrefix, postJsonObject['id'], From 7c2405a9a628964bfac87e805cc44c0c7586cbd1 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 21 Mar 2021 12:44:58 +0000 Subject: [PATCH 69/87] Add to field --- posts.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/posts.py b/posts.py index d82a068a1..2615b9d0c 100644 --- a/posts.py +++ b/posts.py @@ -4155,6 +4155,7 @@ def sendMuteViaServer(baseDir: str, session, "@context": "https://www.w3.org/ns/activitystreams", 'type': 'Ignore', 'actor': actor, + 'to': [actor], 'object': mutedUrl } @@ -4229,9 +4230,11 @@ def sendUndoMuteViaServer(baseDir: str, session, "@context": "https://www.w3.org/ns/activitystreams", 'type': 'Undo', 'actor': actor, + 'to': [actor], 'object': { 'type': 'Ignore', 'actor': actor, + 'to': [actor], 'object': mutedUrl } } From 4ad6eaa0169162a21f53b8ddcbd2adb39ed275e7 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 21 Mar 2021 12:50:05 +0000 Subject: [PATCH 70/87] Ignore activity permitted in outbox --- daemon.py | 2 +- outbox.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/daemon.py b/daemon.py index f87b7a830..adc1d81b4 100644 --- a/daemon.py +++ b/daemon.py @@ -1264,7 +1264,7 @@ class PubServer(BaseHTTPRequestHandler): originalMessageJson = messageJson.copy() - addToFieldTypes = ('Follow', 'Like', 'Add', 'Remove') + addToFieldTypes = ('Follow', 'Like', 'Add', 'Remove', 'Ignore') for addToType in addToFieldTypes: messageJson, toFieldExists = \ addToField(addToType, messageJson, self.server.debug) diff --git a/outbox.py b/outbox.py index 8940cd691..9243c7aa6 100644 --- a/outbox.py +++ b/outbox.py @@ -311,7 +311,8 @@ def postMessageToOutbox(session, translate: {}, permittedOutboxTypes = ('Create', 'Announce', 'Like', 'Follow', 'Undo', 'Update', 'Add', 'Remove', 'Block', 'Delete', - 'Delegate', 'Skill', 'Add', 'Remove', 'Event') + 'Delegate', 'Skill', 'Add', 'Remove', 'Event', + 'Ignore') if messageJson['type'] not in permittedOutboxTypes: if debug: print('DEBUG: POST to outbox - ' + messageJson['type'] + From 032ebe2a8ce332942c5a04f83ee01c0618d709a9 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 21 Mar 2021 13:17:59 +0000 Subject: [PATCH 71/87] Refresh timeline after mute --- bookmarks.py | 4 ++-- desktop_client.py | 21 +++++++++++++++++++++ posts.py | 8 ++++---- session.py | 6 ++++++ 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/bookmarks.py b/bookmarks.py index f69d4c7bc..f4e292c3a 100644 --- a/bookmarks.py +++ b/bookmarks.py @@ -418,7 +418,7 @@ def sendBookmarkViaServer(baseDir: str, session, 'Authorization': authHeader } postResult = postJson(session, newBookmarkJson, [], inboxUrl, - headers, 30, True) + headers, 3, True) if not postResult: if debug: print('WARN: POST bookmark failed for c2s to ' + inboxUrl) @@ -502,7 +502,7 @@ def sendUndoBookmarkViaServer(baseDir: str, session, 'Authorization': authHeader } postResult = postJson(session, newBookmarkJson, [], inboxUrl, - headers, 30, True) + headers, 3, True) if not postResult: if debug: print('WARN: POST unbookmark failed for c2s to ' + inboxUrl) diff --git a/desktop_client.py b/desktop_client.py index 8163a1942..e242183c5 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -1241,6 +1241,17 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, httpPrefix, postJsonObject['id'], cachedWebfingers, personCache, False, __version__) + + boxJson = c2sBoxJson(baseDir, session, + nickname, password, + domain, port, httpPrefix, + currTimeline, pageNumber, + debug) + if boxJson: + _desktopShowBox(currTimeline, boxJson, + screenreader, systemLanguage, + espeak, pageNumber, + newRepliesExist, newDMsExist) print('') elif (commandStr == 'mute' or commandStr == 'ignore' or @@ -1269,6 +1280,16 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, httpPrefix, postJsonObject['id'], cachedWebfingers, personCache, False, __version__) + boxJson = c2sBoxJson(baseDir, session, + nickname, password, + domain, port, httpPrefix, + currTimeline, pageNumber, + debug) + if boxJson: + _desktopShowBox(currTimeline, boxJson, + screenreader, systemLanguage, + espeak, pageNumber, + newRepliesExist, newDMsExist) print('') elif (commandStr == 'undo bookmark' or commandStr == 'remove bookmark' or diff --git a/posts.py b/posts.py index 2615b9d0c..cf49930fb 100644 --- a/posts.py +++ b/posts.py @@ -4199,8 +4199,8 @@ def sendMuteViaServer(baseDir: str, session, 'Authorization': authHeader } postResult = postJson(session, newMuteJson, [], inboxUrl, - headers, 30, True) - if not postResult: + headers, 3, True) + if postResult is None: print('WARN: mute unable to post') if debug: @@ -4280,8 +4280,8 @@ def sendUndoMuteViaServer(baseDir: str, session, 'Authorization': authHeader } postResult = postJson(session, undoMuteJson, [], inboxUrl, - headers, 30, True) - if not postResult: + headers, 3, True) + if postResult is None: print('WARN: undo mute unable to post') if debug: diff --git a/session.py b/session.py index f4d6e6dd7..16f7d0fe5 100644 --- a/session.py +++ b/session.py @@ -152,6 +152,12 @@ def postJson(session, postJsonObject: {}, federationList: [], session.post(url=inboxUrl, data=json.dumps(postJsonObject), headers=headers, timeout=timeoutSec) + except requests.Timeout as e: + if not quiet: + print('ERROR: postJson timeout ' + inboxUrl + ' ' + + json.dumps(postJsonObject) + ' ' + str(headers)) + print(e) + return '' except requests.exceptions.RequestException as e: if not quiet: print('ERROR: postJson requests failed ' + inboxUrl + ' ' + From b2b1aa60135f87c1c808824b349da3950f685443 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 21 Mar 2021 13:20:07 +0000 Subject: [PATCH 72/87] Bookmark icon --- desktop_client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/desktop_client.py b/desktop_client.py index e242183c5..3cfb63e77 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -705,6 +705,8 @@ def _desktopShowBox(boxName: str, boxJson: {}, content = contentWarning if postJsonObject['object'].get('ignores'): content = '🔇' + if postJsonObject['object'].get('bookmarks'): + content = '🔖' print(indent + str(posStr) + ' | ' + name + ' | ' + published + ' | ' + content) ctr += 1 From 15d62ab6b8e2b5bb8bef1267e7eb7f0cdd7a12f5 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 21 Mar 2021 13:29:56 +0000 Subject: [PATCH 73/87] Refresh timeline after some commands --- desktop_client.py | 67 ++++++++++++++++++----------------------------- 1 file changed, 26 insertions(+), 41 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index 3cfb63e77..62bbea52a 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -994,6 +994,8 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, else: commandStr = _desktopWaitForCmd(30, debug) if commandStr: + refreshTimeline = False + if commandStr.startswith('/'): commandStr = commandStr[1:] if commandStr == 'q' or \ @@ -1072,29 +1074,11 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, pageNumber = 1 prevTimelineFirstId = '' currTimeline = 'inbox' - boxJson = c2sBoxJson(baseDir, session, - nickname, password, - domain, port, httpPrefix, - currTimeline, pageNumber, - debug) - if boxJson: - _desktopShowBox(currTimeline, boxJson, - screenreader, systemLanguage, espeak, - pageNumber, - newRepliesExist, newDMsExist) + refreshTimeline = True elif commandStr.startswith('next'): pageNumber += 1 prevTimelineFirstId = '' - boxJson = c2sBoxJson(baseDir, session, - nickname, password, - domain, port, httpPrefix, - currTimeline, pageNumber, - debug) - if boxJson: - _desktopShowBox(currTimeline, boxJson, - screenreader, systemLanguage, espeak, - pageNumber, - newRepliesExist, newDMsExist) + refreshTimeline = True elif commandStr.startswith('prev'): pageNumber -= 1 if pageNumber < 1: @@ -1139,6 +1123,7 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, debug, subject, screenreader, systemLanguage, espeak) + refreshTimeline = True print('') elif (commandStr == 'post' or commandStr == 'p' or commandStr == 'send' or @@ -1181,6 +1166,7 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, debug, screenreader, systemLanguage, espeak) + refreshTimeline = True print('') elif commandStr == 'like' or commandStr.startswith('like '): currIndex = 0 @@ -1206,6 +1192,7 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, postJsonObject['id'], cachedWebfingers, personCache, False, __version__) + refreshTimeline = True print('') elif (commandStr == 'undo mute' or commandStr == 'undo ignore' or @@ -1243,17 +1230,7 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, httpPrefix, postJsonObject['id'], cachedWebfingers, personCache, False, __version__) - - boxJson = c2sBoxJson(baseDir, session, - nickname, password, - domain, port, httpPrefix, - currTimeline, pageNumber, - debug) - if boxJson: - _desktopShowBox(currTimeline, boxJson, - screenreader, systemLanguage, - espeak, pageNumber, - newRepliesExist, newDMsExist) + refreshTimeline = True print('') elif (commandStr == 'mute' or commandStr == 'ignore' or @@ -1282,16 +1259,7 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, httpPrefix, postJsonObject['id'], cachedWebfingers, personCache, False, __version__) - boxJson = c2sBoxJson(baseDir, session, - nickname, password, - domain, port, httpPrefix, - currTimeline, pageNumber, - debug) - if boxJson: - _desktopShowBox(currTimeline, boxJson, - screenreader, systemLanguage, - espeak, pageNumber, - newRepliesExist, newDMsExist) + refreshTimeline = True print('') elif (commandStr == 'undo bookmark' or commandStr == 'remove bookmark' or @@ -1332,6 +1300,7 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, cachedWebfingers, personCache, False, __version__) + refreshTimeline = True print('') elif (commandStr == 'bookmark' or commandStr == 'bm' or @@ -1360,6 +1329,7 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, postJsonObject['id'], cachedWebfingers, personCache, False, __version__) + refreshTimeline = True print('') elif commandStr == 'unlike' or commandStr == 'undo like': currIndex = 0 @@ -1386,6 +1356,7 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, postJsonObject['id'], cachedWebfingers, personCache, False, __version__) + refreshTimeline = True print('') elif (commandStr.startswith('announce') or commandStr.startswith('boost') or @@ -1415,6 +1386,7 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, httpPrefix, postId, cachedWebfingers, personCache, True, __version__) + refreshTimeline = True print('') elif (commandStr.startswith('unannounce') or commandStr.startswith('undo announce') or @@ -1448,6 +1420,7 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, cachedWebfingers, personCache, True, __version__) + refreshTimeline = True print('') elif commandStr.startswith('follow '): followHandle = commandStr.replace('follow ', '').strip() @@ -1606,3 +1579,15 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, print('') elif commandStr.startswith('h'): _desktopHelp() + + if refreshTimeline: + boxJson = c2sBoxJson(baseDir, session, + nickname, password, + domain, port, httpPrefix, + currTimeline, pageNumber, + debug) + if boxJson: + _desktopShowBox(currTimeline, boxJson, + screenreader, systemLanguage, + espeak, pageNumber, + newRepliesExist, newDMsExist) From 54efec157a9c4371fd4788eb69388b8be2dcd5da Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 21 Mar 2021 13:35:15 +0000 Subject: [PATCH 74/87] Smaller timeouts --- announce.py | 4 ++-- follow.py | 4 ++-- like.py | 4 ++-- posts.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/announce.py b/announce.py index 01212264e..435cd911e 100644 --- a/announce.py +++ b/announce.py @@ -258,7 +258,7 @@ def sendAnnounceViaServer(baseDir: str, session, 'Authorization': authHeader } postResult = postJson(session, newAnnounceJson, [], inboxUrl, - headers, 30, True) + headers, 3, True) if not postResult: print('WARN: announce not posted') @@ -337,7 +337,7 @@ def sendUndoAnnounceViaServer(baseDir: str, session, 'Authorization': authHeader } postResult = postJson(session, unAnnounceJson, [], inboxUrl, - headers, 30, True) + headers, 3, True) if not postResult: print('WARN: undo announce not posted') diff --git a/follow.py b/follow.py index ab2b5d595..51b116a89 100644 --- a/follow.py +++ b/follow.py @@ -1026,7 +1026,7 @@ def sendFollowRequestViaServer(baseDir: str, session, 'Authorization': authHeader } postResult = \ - postJson(session, newFollowJson, [], inboxUrl, headers, 30, True) + postJson(session, newFollowJson, [], inboxUrl, headers, 3, True) if not postResult: if debug: print('DEBUG: POST follow request failed for c2s to ' + inboxUrl) @@ -1119,7 +1119,7 @@ def sendUnfollowRequestViaServer(baseDir: str, session, 'Authorization': authHeader } postResult = \ - postJson(session, unfollowJson, [], inboxUrl, headers, 30, True) + postJson(session, unfollowJson, [], inboxUrl, headers, 3, True) if not postResult: if debug: print('DEBUG: POST unfollow failed for c2s to ' + inboxUrl) diff --git a/like.py b/like.py index 2bc561de5..f009cbd20 100644 --- a/like.py +++ b/like.py @@ -204,7 +204,7 @@ def sendLikeViaServer(baseDir: str, session, 'Authorization': authHeader } postResult = postJson(session, newLikeJson, [], inboxUrl, - headers, 30, True) + headers, 3, True) if not postResult: if debug: print('WARN: POST like failed for c2s to ' + inboxUrl) @@ -286,7 +286,7 @@ def sendUndoLikeViaServer(baseDir: str, session, 'Authorization': authHeader } postResult = postJson(session, newUndoLikeJson, [], inboxUrl, - headers, 30, True) + headers, 3, True) if not postResult: if debug: print('WARN: POST unlike failed for c2s to ' + inboxUrl) diff --git a/posts.py b/posts.py index cf49930fb..74ea9133b 100644 --- a/posts.py +++ b/posts.py @@ -2130,7 +2130,7 @@ def sendPostViaServer(projectVersion: str, postDumps = json.dumps(postJsonObject) postResult = \ postJsonString(session, postDumps, [], - inboxUrl, headers, debug, 60, True) + inboxUrl, headers, debug, 5, True) if not postResult: if debug: print('DEBUG: POST failed for c2s to ' + inboxUrl) From 47c39b408b17b2423c2da073a038e672f92016fa Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 21 Mar 2021 13:43:04 +0000 Subject: [PATCH 75/87] Refreshing timeline after DM --- desktop_client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/desktop_client.py b/desktop_client.py index 62bbea52a..c0a95f492 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -328,6 +328,7 @@ def _desktopNewPost(session, sayStr = 'No post was entered.' _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) return + print('') sayStr = 'You entered this public post:' _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) _sayCommand(newMessage, newMessage, screenreader, systemLanguage, espeak) @@ -1157,6 +1158,7 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, debug, screenreader, systemLanguage, espeak) + refreshTimeline = True else: # public post _desktopNewPost(sessionPost, @@ -1166,7 +1168,7 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, debug, screenreader, systemLanguage, espeak) - refreshTimeline = True + refreshTimeline = True print('') elif commandStr == 'like' or commandStr.startswith('like '): currIndex = 0 From 681cb126eeedc9833261d37c93ce808dbbacabbe Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 21 Mar 2021 16:09:18 +0000 Subject: [PATCH 76/87] Show content after bookmark --- desktop_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop_client.py b/desktop_client.py index c0a95f492..9952a9c89 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -707,7 +707,7 @@ def _desktopShowBox(boxName: str, boxJson: {}, if postJsonObject['object'].get('ignores'): content = '🔇' if postJsonObject['object'].get('bookmarks'): - content = '🔖' + content = '🔖' + content print(indent + str(posStr) + ' | ' + name + ' | ' + published + ' | ' + content) ctr += 1 From 800ff69f9afa87f3e49ccf93a7e824db5a01c074 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 21 Mar 2021 18:37:06 +0000 Subject: [PATCH 77/87] Delete posts via desktop client --- delete.py | 2 +- desktop_client.py | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/delete.py b/delete.py index d904c5275..7f9380a4d 100644 --- a/delete.py +++ b/delete.py @@ -92,7 +92,7 @@ def sendDeleteViaServer(baseDir: str, session, 'Authorization': authHeader } postResult = \ - postJson(session, newDeleteJson, [], inboxUrl, headers, 30, True) + postJson(session, newDeleteJson, [], inboxUrl, headers, 3, True) if not postResult: if debug: print('DEBUG: POST delete failed for c2s to ' + inboxUrl) diff --git a/desktop_client.py b/desktop_client.py index 9952a9c89..b41e29766 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -14,6 +14,7 @@ import select import webbrowser import urllib.parse from random import randint +from utils import getFullDomain from utils import isDM from utils import loadTranslationsFromFile from utils import removeHtml @@ -43,6 +44,7 @@ from pgp import pgpPublicKeyUpload from like import noOfLikes from bookmarks import sendBookmarkViaServer from bookmarks import sendUndoBookmarkViaServer +from delete import sendDeleteViaServer def _desktopHelp() -> None: @@ -1581,6 +1583,44 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, print('') elif commandStr.startswith('h'): _desktopHelp() + elif (commandStr == 'delete' or + commandStr == 'rm' or + commandStr.startswith('delete ') or + commandStr.startswith('rm ')): + currIndex = 0 + if ' ' in commandStr: + postIndex = commandStr.split(' ')[-1].strip() + if postIndex.isdigit(): + currIndex = int(postIndex) + if currIndex > 0 and boxJson: + postJsonObject = \ + _desktopGetBoxPostObject(boxJson, currIndex) + if postJsonObject: + if postJsonObject.get('id'): + domainFull = getFullDomain(domain, port) + actor = httpPrefix + '://' + \ + domainFull + '/users/' + nickname + rmActor = postJsonObject['object']['attributedTo'] + if rmActor != actor: + sayStr = 'You can only delete your own posts' + _sayCommand(sayStr, sayStr, + screenreader, + systemLanguage, espeak) + else: + sayStr = 'Deleting post' + _sayCommand(sayStr, sayStr, + screenreader, + systemLanguage, espeak) + sessionrm = createSession(proxyType) + sendDeleteViaServer(baseDir, sessionrm, + nickname, password, + domain, port, + httpPrefix, + postJsonObject['id'], + cachedWebfingers, personCache, + False, __version__) + refreshTimeline = True + print('') if refreshTimeline: boxJson = c2sBoxJson(baseDir, session, From 221bf8e4b8f5ef2fa99e349c6f8ca395c0e685ed Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 21 Mar 2021 20:05:06 +0000 Subject: [PATCH 78/87] Confirm post deletion --- desktop_client.py | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index b41e29766..913312973 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -1607,19 +1607,30 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, screenreader, systemLanguage, espeak) else: - sayStr = 'Deleting post' - _sayCommand(sayStr, sayStr, - screenreader, + print('') + if postJsonObject['object'].get('summary'): + print(postJsonObject['object']['summary']) + print(postJsonObject['object']['content']) + print('') + sayStr = 'Confirm delete, yes or no?' + _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) - sessionrm = createSession(proxyType) - sendDeleteViaServer(baseDir, sessionrm, - nickname, password, - domain, port, - httpPrefix, - postJsonObject['id'], - cachedWebfingers, personCache, - False, __version__) - refreshTimeline = True + yesno = input() + if 'y' not in yesno.lower(): + sayStr = 'Deleting post' + _sayCommand(sayStr, sayStr, + screenreader, + systemLanguage, espeak) + sessionrm = createSession(proxyType) + sendDeleteViaServer(baseDir, sessionrm, + nickname, password, + domain, port, + httpPrefix, + postJsonObject['id'], + cachedWebfingers, + personCache, + False, __version__) + refreshTimeline = True print('') if refreshTimeline: From 5a9727ecf91441757e3dd3da3261c7727b0e9c17 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 21 Mar 2021 20:17:56 +0000 Subject: [PATCH 79/87] Simplify --- desktop_client.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index 913312973..91a5912b1 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -1103,6 +1103,10 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, else: postIndexStr = commandStr.split('read ')[1] if boxJson and postIndexStr.isdigit(): + _desktopShowBox(currTimeline, boxJson, + screenreader, systemLanguage, + espeak, pageNumber, + newRepliesExist, newDMsExist) postIndex = int(postIndexStr) postJsonObject = \ _readLocalBoxPost(session, nickname, domain, @@ -1634,11 +1638,6 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, print('') if refreshTimeline: - boxJson = c2sBoxJson(baseDir, session, - nickname, password, - domain, port, httpPrefix, - currTimeline, pageNumber, - debug) if boxJson: _desktopShowBox(currTimeline, boxJson, screenreader, systemLanguage, From f6a07cde7631f053a8a5a8a35288ad09f02e6bd7 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 22 Mar 2021 13:09:17 +0000 Subject: [PATCH 80/87] Re-adding notifications to the desktop client --- desktop_client.py | 171 ++++++++++++++++++++++++++++++++++++++++++---- inbox.py | 10 +-- utils.py | 2 + 3 files changed, 166 insertions(+), 17 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index 91a5912b1..1ec6ff5cf 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -13,6 +13,7 @@ import sys import select import webbrowser import urllib.parse +from pathlib import Path from random import randint from utils import getFullDomain from utils import isDM @@ -103,6 +104,124 @@ def _desktopHelp() -> None: print('') +def _createDesktopConfig(actor: str) -> None: + """Sets up directories for desktop client configuration + """ + homeDir = str(Path.home()) + if not os.path.isdir(homeDir + '/.config'): + os.mkdir(homeDir + '/.config') + if not os.path.isdir(homeDir + '/.config/epicyon'): + os.mkdir(homeDir + '/.config/epicyon') + nickname = getNicknameFromActor(actor) + domain, port = getDomainFromActor(actor) + handle = nickname + '@' + domain + if port != 443 and port != 80: + handle += '_' + str(port) + readPostsDir = homeDir + '/.config/epicyon/' + handle + if not os.path.isdir(readPostsDir): + os.mkdir(readPostsDir) + + +def _markPostAsRead(actor: str, postId: str, postCategory: str) -> None: + """Marks the given post as read by the given actor + """ + homeDir = str(Path.home()) + _createDesktopConfig(actor) + nickname = getNicknameFromActor(actor) + domain, port = getDomainFromActor(actor) + handle = nickname + '@' + domain + if port != 443 and port != 80: + handle += '_' + str(port) + readPostsDir = homeDir + '/.config/epicyon/' + handle + readPostsFilename = readPostsDir + '/' + postCategory + '.txt' + if os.path.isfile(readPostsFilename): + if postId in open(readPostsFilename).read(): + return + try: + # prepend to read posts file + postId += '\n' + with open(readPostsFilename, 'r+') as readFile: + content = readFile.read() + if postId not in content: + readFile.seek(0, 0) + readFile.write(postId + content) + except Exception as e: + print('WARN: Failed to mark post as read' + str(e)) + else: + readFile = open(readPostsFilename, 'w+') + if readFile: + readFile.write(postId + '\n') + readFile.close() + + +def _hasReadPost(actor: str, postId: str, postCategory: str) -> bool: + """Returns true if the given post has been read by the actor + """ + homeDir = str(Path.home()) + _createDesktopConfig(actor) + nickname = getNicknameFromActor(actor) + domain, port = getDomainFromActor(actor) + handle = nickname + '@' + domain + if port != 443 and port != 80: + handle += '_' + str(port) + readPostsDir = homeDir + '/.config/epicyon/' + handle + readPostsFilename = readPostsDir + '/' + postCategory + '.txt' + if os.path.isfile(readPostsFilename): + if postId in open(readPostsFilename).read(): + return True + return False + + +def _postIsToYou(actor: str, postJsonObject: {}) -> bool: + """Returns true if the post is to the actor + """ + toYourActor = False + if postJsonObject.get('to'): + if actor in postJsonObject['to']: + toYourActor = True + if not toYourActor and postJsonObject.get('object'): + if isinstance(postJsonObject['object'], dict): + if postJsonObject['object'].get('to'): + if actor in postJsonObject['object']['to']: + toYourActor = True + return toYourActor + + +def _newDesktopNotifications(actor: str, inboxJson: {}, + notifyJson: {}) -> None: + """Looks for changes in the inbox and adds notifications + """ + if not inboxJson: + return + if not inboxJson.get('orderedItems'): + return + newDM = False + newReply = False + for postJsonObject in inboxJson['orderedItems']: + if not postJsonObject.get('id'): + continue + if not _postIsToYou(actor, postJsonObject): + continue + if 'dmNotify' not in notifyJson: + notifyJson['dmNotify'] = False + if isDM(postJsonObject): + if not newDM: + if not _hasReadPost(actor, postJsonObject['id'], 'dm'): + if notifyJson.get('dmPostId'): + if notifyJson['dmPostId'] != postJsonObject['id']: + notifyJson['dmNotify'] = True + newDM = True + notifyJson['dmPostId'] = postJsonObject['id'] + else: + if not newReply: + if not _hasReadPost(actor, postJsonObject['id'], 'replies'): + if notifyJson.get('repliesPostId'): + if notifyJson['repliesPostId'] != postJsonObject['id']: + notifyJson['repliesNotify'] = True + newReply = True + notifyJson['repliesPostId'] = postJsonObject['id'] + + def _desktopClearScreen() -> None: """Clears the screen """ @@ -439,7 +558,7 @@ def _readLocalBoxPost(session, nickname: str, domain: str, pageNumber: int, index: int, boxJson: {}, systemLanguage: str, screenreader: str, espeak, - translate: {}) -> {}: + translate: {}, yourActor: str) -> {}: """Reads a post from the given timeline Returns the speaker json """ @@ -532,6 +651,14 @@ def _readLocalBoxPost(session, nickname: str, domain: str, _sayCommand(content, messageStr, screenreader, systemLanguage, espeak, nameStr, gender) + + # if the post is addressed to you then mark it as read + if _postIsToYou(yourActor, postJsonObject): + if isDM(postJsonObject['id']): + _markPostAsRead(yourActor, postJsonObject['id'], 'dm') + else: + _markPostAsRead(yourActor, postJsonObject['id'], 'replies') + return postJsonObject @@ -920,8 +1047,8 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, # prevFollow = False # prevLike = '' # prevShare = False - # dmSoundFilename = 'dm.ogg' - # replySoundFilename = 'reply.ogg' + dmSoundFilename = 'dm.ogg' + replySoundFilename = 'reply.ogg' # calendarSoundFilename = 'calendar.ogg' # followSoundFilename = 'follow.ogg' # likeSoundFilename = 'like.ogg' @@ -937,11 +1064,6 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, newDMsExist = False pgpKeyUpload = False - # NOTE: These are dummy calls to make unit tests pass - # they should be removed later - _desktopNotification("", "test", "message") - _playNotificationSound("test83639") - sayStr = indent + 'Loading translations file' _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) @@ -956,6 +1078,11 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, sayStr = indent + '/q or /quit to exit' _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) + + domainFull = getFullDomain(domain, port) + yourActor = httpPrefix + '://' + domainFull + '/users/' + nickname + + notifyJson = {} prevTimelineFirstId = '' while (1): if not pgpKeyUpload: @@ -978,6 +1105,27 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, currTimeline, pageNumber, debug) + if currTimeline != 'inbox': + # monitor the inbox to generate notifications + inboxJson = c2sBoxJson(baseDir, session, + nickname, password, + domain, port, httpPrefix, + 'inbox', pageNumber, + debug) + else: + inboxJson = boxJson + if inboxJson: + _newDesktopNotifications(yourActor, inboxJson, notifyJson) + if notifyJson.get('dmNotify'): + _desktopNotification(notificationType, + "Epicyon", "New DM " + yourActor + '/dm') + _playNotificationSound(dmSoundFilename) + if notifyJson.get('repliesNotify'): + _desktopNotification(notificationType, + "Epicyon", + "New reply " + yourActor + '/replies') + _playNotificationSound(replySoundFilename) + if boxJson: timelineFirstId = _getFirstItemId(boxJson) if timelineFirstId != prevTimelineFirstId: @@ -1113,7 +1261,7 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, httpPrefix, baseDir, currTimeline, pageNumber, postIndex, boxJson, systemLanguage, screenreader, - espeak, translate) + espeak, translate, yourActor) print('') elif commandStr == 'reply' or commandStr == 'r': if postJsonObject: @@ -1601,11 +1749,8 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, _desktopGetBoxPostObject(boxJson, currIndex) if postJsonObject: if postJsonObject.get('id'): - domainFull = getFullDomain(domain, port) - actor = httpPrefix + '://' + \ - domainFull + '/users/' + nickname rmActor = postJsonObject['object']['attributedTo'] - if rmActor != actor: + if rmActor != yourActor: sayStr = 'You can only delete your own posts' _sayCommand(sayStr, sayStr, screenreader, diff --git a/inbox.py b/inbox.py index bef4561d5..670275283 100644 --- a/inbox.py +++ b/inbox.py @@ -1612,12 +1612,14 @@ def populateReplies(baseDir: str, httpPrefix: str, domain: str, return False if messageId not in open(postRepliesFilename).read(): repliesFile = open(postRepliesFilename, 'a+') - repliesFile.write(messageId + '\n') - repliesFile.close() + if repliesFile: + repliesFile.write(messageId + '\n') + repliesFile.close() else: repliesFile = open(postRepliesFilename, 'w+') - repliesFile.write(messageId + '\n') - repliesFile.close() + if repliesFile: + repliesFile.write(messageId + '\n') + repliesFile.close() return True diff --git a/utils.py b/utils.py index bf6a3c54b..ae3b91ccf 100644 --- a/utils.py +++ b/utils.py @@ -253,6 +253,7 @@ def removeHtml(content: str) -> str: if '<' not in content: return content removing = False + content = content.replace('', '"').replace('', '"') result = '' for ch in content: @@ -262,6 +263,7 @@ def removeHtml(content: str) -> str: removing = False elif not removing: result += ch + result = result.replace(' ', ' ') return result From 5ea91d15c2e5eb2d7cbee73d46da41278ec46d0b Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 22 Mar 2021 13:43:20 +0000 Subject: [PATCH 81/87] Notification sounds --- desktop_client.py | 51 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index 1ec6ff5cf..b26118d68 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -210,16 +210,26 @@ def _newDesktopNotifications(actor: str, inboxJson: {}, if notifyJson.get('dmPostId'): if notifyJson['dmPostId'] != postJsonObject['id']: notifyJson['dmNotify'] = True + notifyJson['dmNotifyChanged'] = True newDM = True + else: + notifyJson['dmNotifyChanged'] = False notifyJson['dmPostId'] = postJsonObject['id'] + if newDM: + break else: if not newReply: if not _hasReadPost(actor, postJsonObject['id'], 'replies'): if notifyJson.get('repliesPostId'): if notifyJson['repliesPostId'] != postJsonObject['id']: notifyJson['repliesNotify'] = True + notifyJson['repliesNotifyChanged'] = True newReply = True + else: + notifyJson['repliesNotifyChanged'] = False notifyJson['repliesPostId'] = postJsonObject['id'] + if newReply: + break def _desktopClearScreen() -> None: @@ -1042,18 +1052,19 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, postJsonObject = {} originalScreenReader = screenreader + soundsDir = 'theme/default/sounds/' # prevSay = '' # prevCalendar = False # prevFollow = False # prevLike = '' # prevShare = False - dmSoundFilename = 'dm.ogg' - replySoundFilename = 'reply.ogg' - # calendarSoundFilename = 'calendar.ogg' - # followSoundFilename = 'follow.ogg' - # likeSoundFilename = 'like.ogg' - # shareSoundFilename = 'share.ogg' - # player = 'ffplay' + dmSoundFilename = soundsDir + 'dm.ogg' + replySoundFilename = soundsDir + 'reply.ogg' + # calendarSoundFilename = soundsDir + 'calendar.ogg' + # followSoundFilename = soundsDir + 'follow.ogg' + # likeSoundFilename = soundsDir + 'like.ogg' + # shareSoundFilename = soundsDir + 'share.ogg' + player = 'ffplay' nameStr = None gender = None messageStr = None @@ -1082,7 +1093,14 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, domainFull = getFullDomain(domain, port) yourActor = httpPrefix + '://' + domainFull + '/users/' + nickname - notifyJson = {} + notifyJson = { + "dmPostId": "Initial", + "dmNotify": False, + "dmNotifyChanged": False, + "repliesPostId": "Initial", + "repliesNotify": False, + "repliesNotifyChanged": False + } prevTimelineFirstId = '' while (1): if not pgpKeyUpload: @@ -1117,14 +1135,17 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, if inboxJson: _newDesktopNotifications(yourActor, inboxJson, notifyJson) if notifyJson.get('dmNotify'): - _desktopNotification(notificationType, - "Epicyon", "New DM " + yourActor + '/dm') - _playNotificationSound(dmSoundFilename) + if notifyJson.get('dmNotifyChanged'): + _desktopNotification(notificationType, + "Epicyon", + "New DM " + yourActor + '/dm') + _playNotificationSound(dmSoundFilename, player) if notifyJson.get('repliesNotify'): - _desktopNotification(notificationType, - "Epicyon", - "New reply " + yourActor + '/replies') - _playNotificationSound(replySoundFilename) + if notifyJson.get('repliesNotifyChanged'): + _desktopNotification(notificationType, + "Epicyon", + "New reply " + yourActor + '/replies') + _playNotificationSound(replySoundFilename, player) if boxJson: timelineFirstId = _getFirstItemId(boxJson) From cd5c1c35e1509cff7a2d74fe58a7913008251fd5 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 22 Mar 2021 13:44:14 +0000 Subject: [PATCH 82/87] Check first page --- desktop_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop_client.py b/desktop_client.py index b26118d68..34973fe29 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -1123,7 +1123,7 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, currTimeline, pageNumber, debug) - if currTimeline != 'inbox': + if not (currTimeline == 'inbox' and pageNumber == 1): # monitor the inbox to generate notifications inboxJson = c2sBoxJson(baseDir, session, nickname, password, From e1673cfb82b5271d44e939bf5395f1f95736e287 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 22 Mar 2021 13:50:49 +0000 Subject: [PATCH 83/87] Reset changed status --- desktop_client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/desktop_client.py b/desktop_client.py index 34973fe29..5f5aa8a9a 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -197,6 +197,8 @@ def _newDesktopNotifications(actor: str, inboxJson: {}, return newDM = False newReply = False + notifyJson['dmNotifyChanged'] = False + notifyJson['repliesNotifyChanged'] = False for postJsonObject in inboxJson['orderedItems']: if not postJsonObject.get('id'): continue From 84789ac2f2216b14ac79bf68426acc98dbabe170 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 22 Mar 2021 13:51:54 +0000 Subject: [PATCH 84/87] Clear changed status --- desktop_client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index 5f5aa8a9a..be21e7cd5 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -1130,8 +1130,7 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, inboxJson = c2sBoxJson(baseDir, session, nickname, password, domain, port, httpPrefix, - 'inbox', pageNumber, - debug) + 'inbox', 1, debug) else: inboxJson = boxJson if inboxJson: From 96cbed7dd170b62677125f15a9ba033cc4124df9 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 22 Mar 2021 14:36:27 +0000 Subject: [PATCH 85/87] Remove leading space from links --- tests.py | 2 ++ utils.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests.py b/tests.py index 7b187bf6b..f66d6bb6f 100644 --- a/tests.py +++ b/tests.py @@ -2718,6 +2718,8 @@ def testFirstParagraphFromString(): '

This is a test

' + \ '

This is another paragraph

' resultStr = firstParagraphFromString(testStr) + if resultStr != 'This is a test': + print(resultStr) assert resultStr == 'This is a test' testStr = 'Testing without html' diff --git a/utils.py b/utils.py index ae3b91ccf..ee51074fb 100644 --- a/utils.py +++ b/utils.py @@ -263,7 +263,7 @@ def removeHtml(content: str) -> str: removing = False elif not removing: result += ch - result = result.replace(' ', ' ') + result = result.replace(' ', ' ').strip() return result From e145e4da46d2a250503d10094b8fb04f751a5f3c Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 22 Mar 2021 16:56:13 +0000 Subject: [PATCH 86/87] Desktop client notification icons --- desktop_client.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/desktop_client.py b/desktop_client.py index be21e7cd5..ad2b9bdad 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -747,7 +747,12 @@ def _desktopShowBox(boxName: str, boxJson: {}, else: boxNameStr = boxName titleStr = '\33[7m' + boxNameStr.upper() + '\33[0m' - # titleStr += ' page ' + str(pageNumber) + + if newDMs: + notificationIcons += ' 📩' + if newReplies: + notificationIcons += ' 📨' + if notificationIcons: while len(titleStr) < 95 - len(notificationIcons): titleStr += ' ' @@ -1133,15 +1138,19 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, 'inbox', 1, debug) else: inboxJson = boxJson + newDMsExist = False + newRepliesExist = False if inboxJson: _newDesktopNotifications(yourActor, inboxJson, notifyJson) if notifyJson.get('dmNotify'): + newDMsExist = True if notifyJson.get('dmNotifyChanged'): _desktopNotification(notificationType, "Epicyon", "New DM " + yourActor + '/dm') _playNotificationSound(dmSoundFilename, player) if notifyJson.get('repliesNotify'): + newRepliesExist = True if notifyJson.get('repliesNotifyChanged'): _desktopNotification(notificationType, "Epicyon", From 34f0cb47ada7273a2e9d6fee3d74110654350ab6 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 22 Mar 2021 18:27:48 +0000 Subject: [PATCH 87/87] profile command for desktop client --- README_commandline.md | 1 + desktop_client.py | 121 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 111 insertions(+), 11 deletions(-) diff --git a/README_commandline.md b/README_commandline.md index a8b1019c9..57388b8ab 100644 --- a/README_commandline.md +++ b/README_commandline.md @@ -471,6 +471,7 @@ next Next page in the timeline prev Previous page in the timeline read [post number] Read a post from a timeline open [post number] Open web links within a timeline post +profile [post number] Show profile for the person who made the given post ``` If you have a GPG key configured on your local system and are sending a direct message to someone who has a PGP key (the exported key, not just the key ID) set as a tag on their profile then it will try to encrypt the message automatically. So under some conditions end-to-end encryption is possible, such that the instance server only sees ciphertext. Conversely, for arriving direct messages if they are PGP encrypted then the desktop client will try to obtain the relevant public key and decrypt. diff --git a/desktop_client.py b/desktop_client.py index ad2b9bdad..9bd3f5c98 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -46,6 +46,7 @@ from like import noOfLikes from bookmarks import sendBookmarkViaServer from bookmarks import sendUndoBookmarkViaServer from delete import sendDeleteViaServer +from person import getActorJson def _desktopHelp() -> None: @@ -101,6 +102,8 @@ def _desktopHelp() -> None: 'Read a post from a timeline') print(indent + 'open [post number] ' + 'Open web links within a timeline post') + print(indent + 'profile [post number] ' + + 'Show profile for the person who made the given post') print('') @@ -572,7 +575,7 @@ def _readLocalBoxPost(session, nickname: str, domain: str, screenreader: str, espeak, translate: {}, yourActor: str) -> {}: """Reads a post from the given timeline - Returns the speaker json + Returns the post json """ if _timelineIsEmpty(boxJson): return {} @@ -650,8 +653,7 @@ def _readLocalBoxPost(session, nickname: str, domain: str, # say the speaker's name _sayCommand(nameStr, nameStr, screenreader, - systemLanguage, espeak, - nameStr, gender) + systemLanguage, espeak, nameStr, gender) if postJsonObject['object'].get('inReplyTo'): print('Replying to ' + postJsonObject['object']['inReplyTo'] + '\n') @@ -661,8 +663,7 @@ def _readLocalBoxPost(session, nickname: str, domain: str, # speak the post content _sayCommand(content, messageStr, screenreader, - systemLanguage, espeak, - nameStr, gender) + systemLanguage, espeak, nameStr, gender) # if the post is addressed to you then mark it as read if _postIsToYou(yourActor, postJsonObject): @@ -674,6 +675,73 @@ def _readLocalBoxPost(session, nickname: str, domain: str, return postJsonObject +def _showProfile(session, nickname: str, domain: str, + httpPrefix: str, baseDir: str, boxName: str, + pageNumber: int, index: int, boxJson: {}, + systemLanguage: str, + screenreader: str, espeak, + translate: {}, yourActor: str) -> {}: + """Shows the profile of the actor for the given post + Returns the actor json + """ + if _timelineIsEmpty(boxJson): + return {} + + postJsonObject = _desktopGetBoxPostObject(boxJson, index) + if not postJsonObject: + return {} + + actor = None + if postJsonObject['type'] == 'Announce': + nickname = getNicknameFromActor(postJsonObject['object']) + if nickname: + nickStr = '/' + nickname + '/' + if nickStr in postJsonObject['object']: + actor = \ + postJsonObject['object'].split(nickStr)[0] + \ + '/' + nickname + else: + actor = postJsonObject['object']['attributedTo'] + + if not actor: + return {} + + isHttp = False + if 'http://' in actor: + isHttp = True + actorJson = getActorJson(actor, isHttp, False, False, True) + + actor = actorJson['id'] + actorNickname = getNicknameFromActor(actor) + actorDomain, actorPort = getDomainFromActor(actor) + actorDomainFull = getFullDomain(actorDomain, actorPort) + handle = '@' + actorNickname + '@' + actorDomainFull + + sayStr = handle + _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) + print(actor) + if actorJson.get('movedTo'): + sayStr = 'Moved to ' + actorJson['movedTo'] + _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) + if actorJson.get('alsoKnownAs'): + alsoKnownAsStr = '' + ctr = 0 + for altActor in actorJson['alsoKnownAs']: + if ctr > 0: + alsoKnownAsStr += ', ' + ctr += 1 + alsoKnownAsStr += altActor + + sayStr = 'Also known as ' + alsoKnownAsStr + _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) + if actorJson.get('summary'): + sayStr = removeHtml(actorJson['summary']) + sayStr2 = speakableText(baseDir, sayStr, translate) + _sayCommand(sayStr, sayStr2, screenreader, systemLanguage, espeak) + + return actorJson + + def _desktopGetBoxPostObject(boxJson: {}, index: int) -> {}: """Gets the post with the given index from the timeline """ @@ -1099,6 +1167,7 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, domainFull = getFullDomain(domain, port) yourActor = httpPrefix + '://' + domainFull + '/users/' + nickname + actorJson = None notifyJson = { "dmPostId": "Initial", @@ -1294,6 +1363,24 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, systemLanguage, screenreader, espeak, translate, yourActor) print('') + elif commandStr.startswith('profile ') or commandStr == 'profile': + if commandStr == 'profile': + postIndexStr = '1' + else: + postIndexStr = commandStr.split('profile ')[1] + if boxJson and postIndexStr.isdigit(): + _desktopShowBox(currTimeline, boxJson, + screenreader, systemLanguage, + espeak, pageNumber, + newRepliesExist, newDMsExist) + postIndex = int(postIndexStr) + actorJson = \ + _showProfile(session, nickname, domain, + httpPrefix, baseDir, currTimeline, + pageNumber, postIndex, boxJson, + systemLanguage, screenreader, + espeak, translate, yourActor) + print('') elif commandStr == 'reply' or commandStr == 'r': if postJsonObject: if postJsonObject.get('id'): @@ -1609,10 +1696,18 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, True, __version__) refreshTimeline = True print('') - elif commandStr.startswith('follow '): - followHandle = commandStr.replace('follow ', '').strip() - if followHandle.startswith('@'): - followHandle = followHandle[1:] + elif (commandStr == 'follow' or + commandStr.startswith('follow ')): + if commandStr == 'follow': + if actorJson: + followHandle = actorJson['id'] + else: + followHandle = '' + else: + followHandle = commandStr.replace('follow ', '').strip() + if followHandle.startswith('@'): + followHandle = followHandle[1:] + if '@' in followHandle or '://' in followHandle: followNickname = getNicknameFromActor(followHandle) followDomain, followPort = \ @@ -1623,7 +1718,8 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) sessionFollow = createSession(proxyType) - sendFollowRequestViaServer(baseDir, sessionFollow, + sendFollowRequestViaServer(baseDir, + sessionFollow, nickname, password, domain, port, followNickname, @@ -1634,7 +1730,10 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, personCache, debug, __version__) else: - sayStr = followHandle + ' is not valid' + if followHandle: + sayStr = followHandle + ' is not valid' + else: + sayStr = 'Specify a handle to follow' _sayCommand(sayStr, screenreader, systemLanguage, espeak) print('')