From 0837b1b67a403476b3b82e187019f86ff44374f8 Mon Sep 17 00:00:00 2001 From: Scott Small Date: Mon, 20 Mar 2023 17:33:12 -0700 Subject: [PATCH] Macstodon 1.0 Release --- AuthHandler.py | 1 - AuthWindows.py | 1 + CHANGELOG.md | 25 ++++++++++++++++++++- ImageHandler.py | 2 +- Macstodon.py | 2 +- Macstodon.rsrc.sit.hqx | 2 +- MacstodonConstants.py | 2 +- MacstodonHelpers.py | 2 +- MacstodonSplash.py | 2 +- PrefsWindow.py | 1 + ProfileWindow.py | 1 + README.md | 34 +++++++++++++++++++---------- TimelineHandler.py | 1 - TimelineWindow.py | 1 + TootHandler.py | 1 - TootWindow.py | 1 + readme_screenshots/attachments.png | Bin 0 -> 12645 bytes readme_screenshots/links.png | Bin 0 -> 11571 bytes readme_screenshots/prefs.png | Bin 0 -> 9270 bytes readme_screenshots/profile.png | Bin 0 -> 73896 bytes readme_screenshots/timeline.png | Bin 40594 -> 43366 bytes third_party/BeautifulSoup.py | 1 - 22 files changed, 57 insertions(+), 23 deletions(-) delete mode 100644 AuthHandler.py create mode 100755 AuthWindows.py mode change 100644 => 100755 CHANGELOG.md mode change 100644 => 100755 ImageHandler.py mode change 100644 => 100755 Macstodon.py mode change 100644 => 100755 Macstodon.rsrc.sit.hqx mode change 100644 => 100755 MacstodonConstants.py mode change 100644 => 100755 MacstodonHelpers.py mode change 100644 => 100755 MacstodonSplash.py create mode 100755 PrefsWindow.py create mode 100755 ProfileWindow.py mode change 100644 => 100755 README.md delete mode 100644 TimelineHandler.py create mode 100755 TimelineWindow.py delete mode 100644 TootHandler.py create mode 100755 TootWindow.py create mode 100644 readme_screenshots/attachments.png create mode 100644 readme_screenshots/links.png create mode 100644 readme_screenshots/prefs.png create mode 100644 readme_screenshots/profile.png delete mode 100755 third_party/BeautifulSoup.py diff --git a/AuthHandler.py b/AuthHandler.py deleted file mode 100644 index b5e45a2..0000000 --- a/AuthHandler.py +++ /dev/null @@ -1 +0,0 @@ -""" Macstodon - a Mastodon client for classic Mac OS MIT License Copyright (c) 2022-2023 Scott Small and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # ############## # Python Imports # ############## import ic import string import urllib import W # ########## # My Imports # ########## from MacstodonConstants import INITIAL_TOOTS, VERSION from MacstodonHelpers import dprint, handleRequest, okDialog # ########### # Application # ########### class AuthHandler: def __init__(self, app): """ Initializes the AuthHandler class. """ self.app = app # ######################### # Window Handling Functions # ######################### def getAuthWindow(self, url): """ Defines the OAuth code window """ authwindow = W.Dialog((320, 260), "Macstodon %s - Auth Code" % VERSION) auth_text_1 = "Please log in to Mastodon using your web browser, it should have " \ "been automatically launched.\r\rIf your web browser did not automatically " \ "open, or if you closed it, you can manually copy the auth URL below:" authwindow.auth_label_1 = W.TextBox((10, 8, -10, 96), auth_text_1) authwindow.url = W.EditText((10, 82, -10, 60), url, readonly=1) auth_text_2 = "After logging in through the web, you will be presented with a " \ "code, please copy that code and paste it below, then press OK." authwindow.auth_label_2 = W.TextBox((10, 150, -10, 96), auth_text_2) authwindow.code = W.EditText((10, 192, -10, 30)) authwindow.logout_btn = W.Button((10, -22, 60, 16), "Logout", self.authLogoutCallback) authwindow.ok_btn = W.Button((-69, -22, 60, 16), "OK", self.authTokenCallback) authwindow.setdefaultbutton(authwindow.ok_btn) return authwindow def getLoginWindow(self): """ Defines the login window (prompts for a server URL) """ prefs = self.app.getprefs() loginwindow = W.Dialog((220, 60), "Macstodon %s - Server" % VERSION) loginwindow.sv_label = W.TextBox((0, 8, 110, 16), "Server URL:") loginwindow.server = W.EditText((70, 6, -10, 16), prefs.server or "") loginwindow.login_btn = W.Button((10, -22, 60, 16), "Login", self.loginCallback) loginwindow.quit_btn = W.Button((-69, -22, 60, 16), "Quit", self.app._quit) loginwindow.setdefaultbutton(loginwindow.login_btn) return loginwindow # ################## # Callback Functions # ################## def authLogoutCallback(self): """ Run when the user clicks the "Logout" button from the auth window. Just closes the auth window and reopens the login window. """ self.app.authwindow.close() self.app.loginwindow = self.getLoginWindow() self.app.loginwindow.open() def authTokenCallback(self): """ Run when the user clicks the "OK" button from the auth window. Uses the user's auth code to make a request for a token. """ authcode = self.app.authwindow.code.get() # Raise an error if authcode is blank if string.strip(authcode) == "": okDialog("Please enter the code that was shown in your web browser after you logged in.") return self.app.authwindow.close() prefs = self.app.getprefs() req_data = { "client_id": prefs.client_id, "client_secret": prefs.client_secret, "redirect_uri": "urn:ietf:wg:oauth:2.0:oob", "grant_type": "authorization_code", "code": authcode, "scope": "read write follow" } path = "/oauth/token" data = handleRequest(self.app, path, req_data) if not data: # handleRequest failed (and should have popped an error dialog already) self.app.loginwindow = self.getLoginWindow() self.app.loginwindow.open() return token = data.get('access_token') if token: prefs.token = token prefs.save() dprint("token: %s" % prefs.token) self.app.timelinewindow = self.app.timelinehandler.getTimelineWindow() self.app.timelinewindow.open() self.app.timelinehandler.refreshHomeCallback(INITIAL_TOOTS) self.app.timelinehandler.refreshLocalCallback(INITIAL_TOOTS) self.app.timelinehandler.refreshNotificationsCallback(INITIAL_TOOTS) else: # we got a JSON response, but it didn't have the auth token dprint("ACK! Token is missing from the response :(") dprint("This is what came back from the server:") dprint(data) if data.get("error_description") is not None: okDialog("Server error when getting auth token:\r\r %s" % data['error_description']) elif data.get("error") is not None: okDialog("Server error when getting auth token:\r\r %s" % data['error']) else: okDialog("Server error when getting auth token.") self.app.loginwindow = self.getLoginWindow() self.app.loginwindow.open() return def loginCallback(self): """ Run when the user clicks the "Login" button from the login window. """ # Raise an error if URL is blank if string.strip(self.app.loginwindow.server.get()) == "": okDialog("Please enter the URL to your Mastodon server, using HTTP instead of HTTPS. This value cannot be blank.") return # Save server to prefs and clear tokens if it has changed prefs = self.app.getprefs() if prefs.server and (self.app.loginwindow.server.get() != prefs.server): dprint("Server has changed, clearing tokens") prefs.token = None prefs.client_id = None prefs.client_secret = None prefs.max_toot_chars = None prefs.server = self.app.loginwindow.server.get() prefs.save() # Close login window self.app.loginwindow.close() # If it's our first time using this server we need to # register our app with it. if prefs.client_id and prefs.client_secret: dprint("Using existing app credentials for this server") else: dprint("First time using this server, creating new app credentials") result = self.createAppCredentials() if not result: self.app.loginwindow = self.getLoginWindow() self.app.loginwindow.open() return dprint("client id: %s" % prefs.client_id) dprint("client secret: %s" % prefs.client_secret) # Do we have a token (is the user logged in?) If not, we need to # log the user in and generate a token if not prefs.token: dprint("Need to generate a user token") self.getAuthCode() else: dprint("Using existing user token for this server") dprint("token: %s" % prefs.token) self.app.timelinewindow = self.app.timelinehandler.getTimelineWindow() self.app.timelinewindow.open() self.app.timelinehandler.refreshHomeCallback(INITIAL_TOOTS) self.app.timelinehandler.refreshLocalCallback(INITIAL_TOOTS) self.app.timelinehandler.refreshNotificationsCallback(INITIAL_TOOTS) # ####################### # OAuth Support Functions # ####################### def createAppCredentials(self): """ Creates credentials on the user's Mastodon instance for this application """ req_data = { "client_name": "Macstodon", "website": "https://github.com/smallsco/macstodon", "redirect_uris": "urn:ietf:wg:oauth:2.0:oob", "scopes": "read write follow" } path = "/api/v1/apps" data = handleRequest(self.app, path, req_data) if not data: # handleRequest failed and should have popped an error dialog return 0 client_secret = data.get('client_secret') client_id = data.get('client_id') if client_secret and client_id: prefs = self.app.getprefs() prefs.client_id = client_id prefs.client_secret = client_secret prefs.save() return 1 else: # we got a JSON response but it didn't have the client ID or secret dprint("ACK! Client ID and/or Client Secret are missing :(") dprint("This is what came back from the server:") dprint(data) if data.get("error_description") is not None: okDialog("Server error when creating application credentials:\r\r %s" % data['error_description']) elif data.get("error") is not None: okDialog("Server error when creating application credentials:\r\r %s" % data['error']) else: okDialog("Server error when creating application credentials.") return 0 def getAuthCode(self): """ Launches the user's web browser with the OAuth token generation URL for their instance. """ prefs = self.app.getprefs() req_data = { "client_id": prefs.client_id, "redirect_uri": "urn:ietf:wg:oauth:2.0:oob", "scope": "read write follow", "response_type": "code" } url = "%s/oauth/authorize?%s" % (prefs.server, urllib.urlencode(req_data)) try: ic.launchurl(url) except: # Internet Config is not installed, or, if it is installed, # then a default web browser has not been set. dprint("Missing Internet Config, or no default browser configured") self.app.authwindow = self.getAuthWindow(url) self.app.authwindow.open() \ No newline at end of file diff --git a/AuthWindows.py b/AuthWindows.py new file mode 100755 index 0000000..071c294 --- /dev/null +++ b/AuthWindows.py @@ -0,0 +1 @@ +""" Macstodon - a Mastodon client for classic Mac OS MIT License Copyright (c) 2022-2023 Scott Small and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # ############## # Python Imports # ############## import ic import string import urllib import W # ########## # My Imports # ########## from MacstodonConstants import VERSION from MacstodonHelpers import dprint, handleRequest, okDialog # ########## # AuthWindow # ########## class AuthWindow(W.Dialog): def __init__(self, url): """ Initializes the AuthWindow class. """ W.Dialog.__init__(self, (320, 260), "Macstodon %s - Auth Code" % VERSION) self.setupwidgets(url) # ######################### # Window Handling Functions # ######################### def setupwidgets(self, url): """ Defines the OAuth code window """ auth_text_1 = "Please log in to Mastodon using your web browser, it should have " \ "been automatically launched.\r\rIf your web browser did not automatically " \ "open, or if you closed it, you can manually copy the auth URL below:" self.auth_label_1 = W.TextBox((10, 8, -10, 96), auth_text_1) self.url = W.EditText((10, 82, -10, 60), url, readonly=1) auth_text_2 = "After logging in through the web, you will be presented with a " \ "code, please copy that code and paste it below, then press OK." self.auth_label_2 = W.TextBox((10, 150, -10, 96), auth_text_2) self.code = W.EditText((10, 192, -10, 30)) self.logout_btn = W.Button((10, -22, 60, 16), "Logout", self.authLogoutCallback) self.ok_btn = W.Button((-69, -22, 60, 16), "OK", self.authTokenCallback) self.setdefaultbutton(self.ok_btn) # ################## # Callback Functions # ################## def authLogoutCallback(self): """ Run when the user clicks the "Logout" button from the auth window. Just closes the auth window and reopens the login window. """ self.parent.loginwindow = LoginWindow() self.parent.loginwindow.open() self.close() def authTokenCallback(self): """ Run when the user clicks the "OK" button from the auth window. Uses the user's auth code to make a request for a token. """ authcode = self.code.get() # Raise an error if authcode is blank if string.strip(authcode) == "": okDialog("Please enter the code that was shown in your web browser after you logged in.") return app = self.parent self.close() prefs = app.getprefs() req_data = { "client_id": prefs.client_id, "client_secret": prefs.client_secret, "redirect_uri": "urn:ietf:wg:oauth:2.0:oob", "grant_type": "authorization_code", "code": authcode, "scope": "read write follow" } path = "/oauth/token" data = handleRequest(app, path, req_data) if not data: # handleRequest failed (and should have popped an error dialog already) app.loginwindow = LoginWindow() app.loginwindow.open() return token = data.get('access_token') if token: prefs.token = token prefs.save() dprint("token: %s" % prefs.token) app.timelinewindow = app.TimelineWindow() app.timelinewindow.open() else: # we got a JSON response, but it didn't have the auth token dprint("ACK! Token is missing from the response :(") dprint("This is what came back from the server:") dprint(data) if data.get("error_description") is not None: okDialog("Server error when getting auth token:\r\r %s" % data['error_description']) elif data.get("error") is not None: okDialog("Server error when getting auth token:\r\r %s" % data['error']) else: okDialog("Server error when getting auth token.") app.loginwindow = LoginWindow() app.loginwindow.open() return # ########### # LoginWindow # ########### class LoginWindow(W.Dialog): def __init__(self): """ Initializes the LoginWindow class. """ W.Dialog.__init__(self, (220, 60), "Macstodon %s - Server" % VERSION) self.setupwidgets() # ######################### # Window Handling Functions # ######################### def setupwidgets(self): """ Defines the login window (prompts for a server URL) """ prefs = self.parent.getprefs() self.sv_label = W.TextBox((0, 8, 110, 16), "Server URL:") self.server = W.EditText((70, 6, -10, 16), prefs.server or "") self.login_btn = W.Button((10, -22, 60, 16), "Login", self.loginCallback) self.quit_btn = W.Button((-69, -22, 60, 16), "Quit", self.parent._quit) self.setdefaultbutton(self.login_btn) # ################## # Callback Functions # ################## def loginCallback(self): """ Run when the user clicks the "Login" button from the login window. """ # Raise an error if URL is blank if string.strip(self.server.get()) == "": okDialog( "Please enter the URL to your Mastodon server, using HTTP instead of HTTPS. This value cannot be blank." ) return # Save server to prefs and clear tokens if it has changed prefs = self.parent.getprefs() if prefs.server and (self.server.get() != prefs.server): dprint("Server has changed, clearing tokens") prefs.token = None prefs.client_id = None prefs.client_secret = None prefs.max_toot_chars = None prefs.server = self.server.get() # Set default prefs if they do not already exist if not prefs.toots_to_load_startup: prefs.toots_to_load_startup = "5" if not prefs.toots_to_load_refresh: prefs.toots_to_load_refresh = "5" if not prefs.toots_per_timeline: prefs.toots_per_timeline = "20" if prefs.show_avatars not in [1, 0]: prefs.show_avatars = 1 if prefs.show_banners not in [1, 0]: prefs.show_banners = 0 # Close login window prefs.save() app = self.parent self.close() # If it's our first time using this server we need to # register our app with it. if prefs.client_id and prefs.client_secret: dprint("Using existing app credentials for this server") else: dprint("First time using this server, creating new app credentials") result = self.createAppCredentials(app) if not result: app.loginwindow = LoginWindow() app.loginwindow.open() return dprint("client id: %s" % prefs.client_id) dprint("client secret: %s" % prefs.client_secret) # Do we have a token (is the user logged in?) If not, we need to # log the user in and generate a token if not prefs.token: dprint("Need to generate a user token") self.getAuthCode(app) else: dprint("Using existing user token for this server") dprint("token: %s" % prefs.token) app.timelinewindow = app.TimelineWindow() app.timelinewindow.open() # ####################### # OAuth Support Functions # ####################### def createAppCredentials(self, app): """ Creates credentials on the user's Mastodon instance for this application """ req_data = { "client_name": "Macstodon", "website": "https://github.com/smallsco/macstodon", "redirect_uris": "urn:ietf:wg:oauth:2.0:oob", "scopes": "read write follow" } path = "/api/v1/apps" data = handleRequest(app, path, req_data) if not data: # handleRequest failed and should have popped an error dialog return 0 client_secret = data.get('client_secret') client_id = data.get('client_id') if client_secret and client_id: prefs = app.getprefs() prefs.client_id = client_id prefs.client_secret = client_secret prefs.save() return 1 else: # we got a JSON response but it didn't have the client ID or secret dprint("ACK! Client ID and/or Client Secret are missing :(") dprint("This is what came back from the server:") dprint(data) if data.get("error_description") is not None: okDialog("Server error when creating application credentials:\r\r %s" % data['error_description']) elif data.get("error") is not None: okDialog("Server error when creating application credentials:\r\r %s" % data['error']) else: okDialog("Server error when creating application credentials.") return 0 def getAuthCode(self, app): """ Launches the user's web browser with the OAuth token generation URL for their instance. """ prefs = app.getprefs() req_data = { "client_id": prefs.client_id, "redirect_uri": "urn:ietf:wg:oauth:2.0:oob", "scope": "read write follow", "response_type": "code" } url = "%s/oauth/authorize?%s" % (prefs.server, urllib.urlencode(req_data)) try: ic.launchurl(url) except: # Internet Config is not installed, or, if it is installed, # then a default web browser has not been set. dprint("Missing Internet Config, or no default browser configured") app.authwindow = AuthWindow(url) app.authwindow.open() \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md old mode 100644 new mode 100755 index 815b09e..87dfe08 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ ## CHANGELOG +### v1.0 (2023-03-20) + +* Implemented support for profiles! Click on a user's profile picture to bring up their profile in a new window. +* Implemented support for user interactions, including follow/unfollow, mute/unmute, and block/unblock. +* Implemented support for viewing and modifying user notes. +* Added a "Find User" button to the Timeline window that allows you to search for a user by name and bring up their profile. +* Clicking on "User" notifications that are not connected to a toot (i.e. "User foobar followed you") will bring up that user's profile. +* Refactored the way that Macstodon does window management, this should result in the app using less memory and running faster. +* Added a Preferences window that allows you to customize the number of toots to load into a timeline, enable/disable images and clear the cache. +* More improvements to unicode character conversion, should see much less "junk" characters now. +* Fixed a bug causing unicode character conversion to not take place in the notification list. +* Removed BeautifulSoup and replaced it with a homebrewed solution for link extraction. +* Added a button to the timeline window for displaying the links attached to a toot, and automatically opening them in your browser. +* Added a button to the timeline window for displaying the attachments on a toot, viewing their alt text, and downloading them to your computer. +* Added new icons for the Boost, Favourite, Bookmark, and Reply buttons, courtesy of [C.M. Harrington](https://mastodon.online/@octothorpe)! +* Fixed a bug where the Toot window would remain open if you logged out with it open. +* Added a menu item for logging out. +* Fixed a JSON parsing bug introduced in 0.4.2, Macstodon will no longer rewrite "false", "true", or "null" if used in a toot. +* Improved reliability of getting the toot character limit from the Mastodon instance. +* The cursor will change to a watch when a progress bar window is active. +* Fixed a bug where the CW field when posting a toot was still usable even when it was invisible. +* Replying to a CW'ed toot will add "re: " to the CW (unless it already starts with that). + ### v0.4.4 (2023-02-23) * Fixed an edge case where the app could crash after authenticating. @@ -46,7 +69,7 @@ ### v0.2.1 (2022-11-20) -* Added a beautiful new application icon by [MhzModels](https://mastodon.art/@mhzmodels) +* Added a beautiful new application icon by [MhzModels](https://artsio.com/@mhzmodels) * Replaced a crash-to-console with a friendly, recoverable error message if the connection is lost during an HTTP request. ### v0.2 (2022-11-19) diff --git a/ImageHandler.py b/ImageHandler.py old mode 100644 new mode 100755 index 23dbf20..905ee7e --- a/ImageHandler.py +++ b/ImageHandler.py @@ -1 +1 @@ -""" Macstodon - a Mastodon client for classic Mac OS MIT License Copyright (c) 2022-2023 Scott Small and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # ############## # Python Imports # ############## import Image import GifImagePlugin import JpegImagePlugin import PngImagePlugin import os import string import sys import urllib import urlparse # ################### # Third-Party Imports # ################### from third_party.PixMapWrapper import PixMapWrapper # ########## # My Imports # ########## from MacstodonHelpers import dprint # ########### # Application # ########### class ImageHandler: def __init__(self, app): """ Initializes the ImageHandler class. """ self.app = app def getImageFromURL(self, url, cache=None): """ Wrapper function - given an image URL, either downloads and caches the image, or loads it from the cache if it already exists there. """ try: if cache: file_name = self.getFilenameFromURL(url) if not self.isCached(file_name, cache): self.downloadImage(url, file_name, cache) img = self.resizeAndGetImage(file_name, cache) else: img = self.getImageFromCache(file_name, cache) else: temp_path = self.downloadImage(url) img = self.resizeAndGetImage(temp_path) urllib.urlcleanup() return img except: etype, evalue = sys.exc_info()[:2] dprint("Error loading image: %s: %s" % (etype, evalue)) return None def getFilenameFromURL(self, url): """ Returns the file name, including extension, from a URL. i.e. http://www.google.ca/foo/bar/baz/asdf.jpg returns "asdf.jpg" """ parsed_url = urlparse.urlparse(url) path = parsed_url[2] file_name = os.path.basename(string.replace(path, "/", ":")) return file_name def isCached(self, file_name, cache): """ Checks if an image exists in cache or not and returns true/false. """ if cache == "account": cachepath = self.app.cacheacctfolderpath elif cache == "media": cachepath = self.app.cachemediafolderpath try: os.stat(os.path.join(cachepath, file_name)) return 1 except: exc_info = sys.exc_info() if str(exc_info[0]) == "mac.error" and exc_info[1][0] == 2: return 0 raise def getImageFromCache(self, file_name, cache): """ Loads an image file from the cache. """ pm = PixMapWrapper() if cache == "account": file_path = os.path.join(self.app.cacheacctfolderpath, file_name) elif cache == "media": file_path = os.path.join(self.app.cachemediafolderpath, file_name) pil_image = Image.open(file_path) pm.fromImage(pil_image) del pil_image return pm def downloadImage(self, url, file_name=None, cache=None): """ Downloads an image from the given URL and writes it to the cache. """ http_url = string.replace(url, "https://", "http://") if cache == "account": file_path = os.path.join(self.app.cacheacctfolderpath, file_name) urllib.urlretrieve(http_url, file_path) elif cache == "media": file_path = os.path.join(self.app.cachemediafolderpath, file_name) urllib.urlretrieve(http_url, file_path) else: file_path = None dest_path, headers = urllib.urlretrieve(http_url) return dest_path def resizeAndGetImage(self, file_name, cache=None): """ Resizes an image to 48x48 pixels and overwrites the existing cached image. """ if cache == "account": file_path = os.path.join(self.app.cacheacctfolderpath, file_name) elif cache == "media": file_path = os.path.join(self.app.cachemediafolderpath, file_name) else: file_path = file_name pil_image = Image.open(file_path) pil_image_small = pil_image.resize((48, 48)) del pil_image pil_image_small.save(file_path) pm = PixMapWrapper() pm.fromImage(pil_image_small) del pil_image_small return pm \ No newline at end of file +""" Macstodon - a Mastodon client for classic Mac OS MIT License Copyright (c) 2022-2023 Scott Small and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # ############## # Python Imports # ############## import Image import GifImagePlugin import JpegImagePlugin import PngImagePlugin import os import string import sys import urllib # ################### # Third-Party Imports # ################### from third_party.PixMapWrapper import PixMapWrapper # ########## # My Imports # ########## from MacstodonHelpers import dprint, getFilenameFromURL # ########### # Application # ########### class ImageHandler: def __init__(self, app): """ Initializes the ImageHandler class. """ self.app = app def getImageFromURL(self, url, cache=None): """ Wrapper function - given an image URL, either downloads and caches the image, or loads it from the cache if it already exists there. """ try: if cache: dprint("Want to cache image: %s" % cache) file_name = getFilenameFromURL(url) if not self.isCached(file_name, cache): dprint("Image is not cached, downloading: %s" % cache) self.downloadImage(url, file_name, cache) dprint("Resizing image: %s" % cache) img = self.resizeAndGetImage(file_name, cache) else: dprint("Image is already cached: %s" % cache) img = self.getImageFromCache(file_name, cache) else: dprint("Do not want to cache image") temp_path = self.downloadImage(url) img = self.resizeAndGetImage(temp_path) urllib.urlcleanup() return img except: etype, evalue = sys.exc_info()[:2] dprint("Error loading image: %s: %s" % (etype, evalue)) return None def isCached(self, file_name, cache): """ Checks if an image exists in cache or not and returns true/false. """ if cache == "account" or cache == "banner": cachepath = self.app.cacheacctfolderpath elif cache == "media": cachepath = self.app.cachemediafolderpath try: os.stat(os.path.join(cachepath, file_name)) return 1 except: exc_info = sys.exc_info() if str(exc_info[0]) == "mac.error" and exc_info[1][0] == 2: return 0 raise def getImageFromCache(self, file_name, cache): """ Loads an image file from the cache. """ pm = PixMapWrapper() if cache == "account" or cache == "banner": file_path = os.path.join(self.app.cacheacctfolderpath, file_name) elif cache == "media": file_path = os.path.join(self.app.cachemediafolderpath, file_name) pil_image = Image.open(file_path) pm.fromImage(pil_image) del pil_image return pm def downloadImage(self, url, file_name=None, cache=None): """ Downloads an image from the given URL and writes it to the cache. """ http_url = string.replace(url, "https://", "http://") if cache == "account" or cache == "banner": file_path = os.path.join(self.app.cacheacctfolderpath, file_name) urllib.urlretrieve(http_url, file_path) elif cache == "media": file_path = os.path.join(self.app.cachemediafolderpath, file_name) urllib.urlretrieve(http_url, file_path) else: file_path = None dest_path, headers = urllib.urlretrieve(http_url) return dest_path def resizeAndGetImage(self, file_name, cache=None): """ Resizes an image to an appropriate size and overwrites the existing cached image. """ if cache == "account" or cache == "banner": file_path = os.path.join(self.app.cacheacctfolderpath, file_name) elif cache == "media": file_path = os.path.join(self.app.cachemediafolderpath, file_name) else: file_path = file_name pil_image = Image.open(file_path) pm = PixMapWrapper() if cache == "account": pil_image_small = pil_image.resize((48, 48)) del pil_image pil_image_small.save(file_path) pm.fromImage(pil_image_small) del pil_image_small elif cache == "banner": pil_image_small = pil_image.resize((384, 128)) del pil_image pil_image_small.save(file_path) pm.fromImage(pil_image_small) del pil_image_small else: pm.fromImage(pil_image) del pil_image return pm \ No newline at end of file diff --git a/Macstodon.py b/Macstodon.py old mode 100644 new mode 100755 index cf19fa3..07ef53d --- a/Macstodon.py +++ b/Macstodon.py @@ -1 +1 @@ -""" Macstodon - a Mastodon client for classic Mac OS MIT License Copyright (c) 2022-2023 Scott Small and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # # macfreeze: exclude msvcrt # macfreeze: exclude SOCKS # macfreeze: exclude TERMIOS # macfreeze: exclude termios # macfreeze: exclude _imaging_gif # macfreeze: exclude Tkinter # # ################################ # Splash Screen - hooks the import # process, so import it first # ################################ import MacstodonSplash # ############## # Python Imports # ############## import AE, AppleEvents import FrameWork import macfs import MacOS import macostools import os import W, Wapplication from MacPrefs import kOnSystemDisk # ########## # My Imports # ########## from MacstodonConstants import DEBUG from AuthHandler import AuthHandler from ImageHandler import ImageHandler from TimelineHandler import TimelineHandler from TootHandler import TootHandler # ########### # Application # ########### class Macstodon(Wapplication.Application): """ The application itself. """ # Creator type of this application MyCreatorType = 'M$dN' # Location of prefs in Preferences Folder preffilepath = ":Macstodon Preferences" # ######################## # Initialization Functions # ######################## def __init__(self): """ Run when the application launches. """ Wapplication.Application.__init__(self, self.MyCreatorType) # All applications should handle these Apple Events, # but you'll need an aete resource. AE.AEInstallEventHandler( # We're already open AppleEvents.kCoreEventClass, AppleEvents.kAEOpenApplication, self.ignoreevent ) AE.AEInstallEventHandler( # No printing in this app AppleEvents.kCoreEventClass, AppleEvents.kAEPrintDocuments, self.ignoreevent ) AE.AEInstallEventHandler( # No opening documents in this app AppleEvents.kCoreEventClass, AppleEvents.kAEOpenDocuments, self.ignoreevent ) AE.AEInstallEventHandler( AppleEvents.kCoreEventClass, AppleEvents.kAEQuitApplication, self.quitevent ) # Splash Screen MacstodonSplash.wait() MacstodonSplash.uninstall_importhook() # Create image cache folders # While the Wapplication framework creates the Macstodon Preferences file # automatically, we have to create the cache folder on our own vrefnum, dirid = macfs.FindFolder(kOnSystemDisk, 'pref', 0) prefsfolder_fss = macfs.FSSpec((vrefnum, dirid, '')) prefsfolder = prefsfolder_fss.as_pathname() path = os.path.join(prefsfolder, ":Macstodon Cache") acctpath = os.path.join(prefsfolder, ":Macstodon Cache:account") mediapath = os.path.join(prefsfolder, ":Macstodon Cache:media") macostools.mkdirs(path) macostools.mkdirs(acctpath) macostools.mkdirs(mediapath) self.cachefolderpath = path self.cacheacctfolderpath = acctpath self.cachemediafolderpath = mediapath # Init handlers self.authhandler = AuthHandler(self) self.imagehandler = ImageHandler(self) self.timelinehandler = TimelineHandler(self) self.toothandler = TootHandler(self) # Open login window self.loginwindow = self.authhandler.getLoginWindow() self.loginwindow.open() # Process some events! self.mainloop() def mainloop(self, mask=FrameWork.everyEvent, wait=0): """ Modified version of Wapplication.mainloop() that removes the debugging/traceback window. """ self.quitting = 0 saveyield = MacOS.EnableAppswitch(-1) try: while not self.quitting: try: self.do1event(mask, wait) except W.AlertError, detail: MacOS.EnableAppswitch(-1) W.Message(detail) except self.DebuggerQuit: MacOS.EnableAppswitch(-1) except: if DEBUG: MacOS.EnableAppswitch(-1) import PyEdit PyEdit.tracebackwindow.traceback() else: raise finally: MacOS.EnableAppswitch(1) def makeusermenus(self): """ Set up menu items which all applications should have. Apple Menu has already been set up. """ # File menu m = Wapplication.Menu(self.menubar, "File") quititem = FrameWork.MenuItem(m, "Quit", "Q", 'quit') # Edit menu m = Wapplication.Menu(self.menubar, "Edit") undoitem = FrameWork.MenuItem(m, "Undo", 'Z', "undo") FrameWork.Separator(m) cutitem = FrameWork.MenuItem(m, "Cut", 'X', "cut") copyitem = FrameWork.MenuItem(m, "Copy", "C", "copy") pasteitem = FrameWork.MenuItem(m, "Paste", "V", "paste") clearitem = FrameWork.MenuItem(m, "Clear", None, "clear") FrameWork.Separator(m) selallitem = FrameWork.MenuItem(m, "Select all", "A", "selectall") # Any other menus would go here # These menu items need to be updated periodically; # any menu item not handled by the application should be here, # as should any with a "can_" handler. self._menustocheck = [ undoitem, cutitem, copyitem, pasteitem, clearitem, selallitem ] # no window menu, so pass def checkopenwindowsmenu(self): pass # ############################## # Apple Event Handling Functions # ############################## def ignoreevent(self, theAppleEvent, theReply): """ Handler for events that we want to ignore """ pass def quitevent(self, theAppleEvent, theReply): """ System is telling us to quit """ self._quit() # ####################### # Menu Handling Functions # ####################### def do_about(self, id, item, window, event): """ User selected "About" from the Apple menu """ MacstodonSplash.about() def domenu_quit(self): """ User selected "Quit" from the File menu """ self._quit() # Run the app Macstodon() \ No newline at end of file +""" Macstodon - a Mastodon client for classic Mac OS MIT License Copyright (c) 2022-2023 Scott Small and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # # macfreeze: exclude msvcrt # macfreeze: exclude SOCKS # macfreeze: exclude TERMIOS # macfreeze: exclude termios # macfreeze: exclude _imaging_gif # macfreeze: exclude Tkinter # # ################################ # Splash Screen - hooks the import # process, so import it first # ################################ import MacstodonSplash # ############## # Python Imports # ############## import AE, AppleEvents import FrameWork import macfs import MacOS import macostools import os import Qd import Res import W, Wapplication from MacPrefs import kOnSystemDisk # ########## # My Imports # ########## from MacstodonConstants import DEBUG from ImageHandler import ImageHandler from AuthWindows import AuthWindow, LoginWindow from PrefsWindow import PrefsWindow from ProfileWindow import ProfileWindow from TimelineWindow import TimelineWindow from TootWindow import TootWindow # ########### # Application # ########### class Macstodon(Wapplication.Application): """ The application itself. """ # Creator type of this application MyCreatorType = 'M$dN' # Location of prefs in Preferences Folder preffilepath = ":Macstodon Preferences" # ######################## # Initialization Functions # ######################## def __init__(self): """ Run when the application launches. """ Wapplication.Application.__init__(self, self.MyCreatorType) # All applications should handle these Apple Events, # but you'll need an aete resource. AE.AEInstallEventHandler( # We're already open AppleEvents.kCoreEventClass, AppleEvents.kAEOpenApplication, self.ignoreevent ) AE.AEInstallEventHandler( # No printing in this app AppleEvents.kCoreEventClass, AppleEvents.kAEPrintDocuments, self.ignoreevent ) AE.AEInstallEventHandler( # No opening documents in this app AppleEvents.kCoreEventClass, AppleEvents.kAEOpenDocuments, self.ignoreevent ) AE.AEInstallEventHandler( AppleEvents.kCoreEventClass, AppleEvents.kAEQuitApplication, self.quitevent ) # Preload Image Resources self.pctBkmDis = Qd.GetPicture(128) # bookmark disabled self.pctBkmBnW = Qd.GetPicture(129) # bookmark b&w self.pctBkmClr = Qd.GetPicture(130) # bookmark color self.pctBstDis = Qd.GetPicture(131) # boost disabled self.pctBstBnW = Qd.GetPicture(132) # boost b&w self.pctBstClr = Qd.GetPicture(133) # boost color self.pctFvtDis = Qd.GetPicture(134) # favourite disabled self.pctFvtBnW = Qd.GetPicture(135) # favourite b&w self.pctFvtClr = Qd.GetPicture(136) # favourite color self.pctRplDis = Qd.GetPicture(137) # reply disabled self.pctRplBnW = Qd.GetPicture(138) # reply b&w self.pctRplClr = Qd.GetPicture(139) # reply color self.pctLnkDis = Qd.GetPicture(140) # links disabled self.pctLnkBnW = Qd.GetPicture(141) # links b&w self.pctAtcDis = Qd.GetPicture(142) # attachments disabled self.pctAtcBnW = Qd.GetPicture(143) # attachments b&w # Splash Screen MacstodonSplash.wait() MacstodonSplash.uninstall_importhook() # Create image cache folders # While the Wapplication framework creates the Macstodon Preferences file # automatically, we have to create the cache folder on our own vrefnum, dirid = macfs.FindFolder(kOnSystemDisk, 'pref', 0) prefsfolder_fss = macfs.FSSpec((vrefnum, dirid, '')) prefsfolder = prefsfolder_fss.as_pathname() path = os.path.join(prefsfolder, ":Macstodon Cache") acctpath = os.path.join(prefsfolder, ":Macstodon Cache:account") mediapath = os.path.join(prefsfolder, ":Macstodon Cache:media") macostools.mkdirs(path) macostools.mkdirs(acctpath) macostools.mkdirs(mediapath) self.cachefolderpath = path self.cacheacctfolderpath = acctpath self.cachemediafolderpath = mediapath # Init handlers self.imagehandler = ImageHandler(self) # Prevent circular imports by window classes self.AuthWindow = AuthWindow self.LoginWindow = LoginWindow self.PrefsWindow = PrefsWindow self.ProfileWindow = ProfileWindow self.TimelineWindow = TimelineWindow self.TootWindow = TootWindow # Variable to hold active profile windows, # so we can auto close them when logging out self.profilewindows = {} # Open login window self.loginwindow = LoginWindow() self.loginwindow.open() # Process some events! self.mainloop() def mainloop(self, mask=FrameWork.everyEvent, wait=0): """ Modified version of Wapplication.mainloop() that removes the debugging/traceback window. """ self.quitting = 0 saveyield = MacOS.EnableAppswitch(-1) try: while not self.quitting: try: self.do1event(mask, wait) except W.AlertError, detail: MacOS.EnableAppswitch(-1) W.Message(detail) except self.DebuggerQuit: MacOS.EnableAppswitch(-1) except: if DEBUG: MacOS.EnableAppswitch(-1) import PyEdit PyEdit.tracebackwindow.traceback() else: raise finally: MacOS.EnableAppswitch(1) def makeusermenus(self): """ Set up menu items which all applications should have. Apple Menu has already been set up. """ # File menu m = Wapplication.Menu(self.menubar, "File") logoutitem = FrameWork.MenuItem(m, "Logout", "L", "logout") quititem = FrameWork.MenuItem(m, "Quit", "Q", "quit") # Edit menu m = Wapplication.Menu(self.menubar, "Edit") undoitem = FrameWork.MenuItem(m, "Undo", 'Z', "undo") FrameWork.Separator(m) cutitem = FrameWork.MenuItem(m, "Cut", 'X', "cut") copyitem = FrameWork.MenuItem(m, "Copy", "C", "copy") pasteitem = FrameWork.MenuItem(m, "Paste", "V", "paste") clearitem = FrameWork.MenuItem(m, "Clear", None, "clear") FrameWork.Separator(m) selallitem = FrameWork.MenuItem(m, "Select all", "A", "selectall") FrameWork.Separator(m) prefsitem = FrameWork.MenuItem(m, "Preferences...", "P", "prefs") # Any other menus would go here # These menu items need to be updated periodically; # any menu item not handled by the application should be here, # as should any with a "can_" handler. self._menustocheck = [ undoitem, cutitem, copyitem, pasteitem, clearitem, selallitem, prefsitem, logoutitem ] # no window menu, so pass def checkopenwindowsmenu(self): pass # ############################## # Apple Event Handling Functions # ############################## def ignoreevent(self, theAppleEvent, theReply): """ Handler for events that we want to ignore """ pass def quitevent(self, theAppleEvent, theReply): """ System is telling us to quit """ self._quit() # ####################### # Menu Handling Functions # ####################### def do_about(self, id, item, window, event): """ User selected "About" from the Apple menu """ MacstodonSplash.about() def domenu_quit(self): """ User selected "Quit" from the File menu """ self._quit() # Run the app # If we're in the Python IDE, need to open the RSRC file try: dummy = Res.GetResource('PICT', 128) except Res.Error: resref = Res.OpenResFile("Macstodon.rsrc") Res.UseResFile(resref) Macstodon() \ No newline at end of file diff --git a/Macstodon.rsrc.sit.hqx b/Macstodon.rsrc.sit.hqx old mode 100644 new mode 100755 index f1219af..120ae46 --- a/Macstodon.rsrc.sit.hqx +++ b/Macstodon.rsrc.sit.hqx @@ -1 +1 @@ -(This file must be converted with BinHex 4.0) :%NeKBh0dEf4[ELjbFh*M,R0TG!"6593e8dP8)3#3"$%'!*!%mh06G(9QCNPd)#K M+6%j16FY-6Nj1#""E'&NC'PZ)&0jFh4PEA-X)%PZBbiX)'KdG(!k,bphN!-ZB@a KC'4TER0jFbjMEfd[8h4eCQC*G#m0#KS!"4!!!$%'!*!$FJ!"!*!$FYEV$D@P8Q9 cCA*fC@5PT3#PN!3"!!!q!!$Ibb-2i"e*kJ#3$3jY,`#3$NeKBh0dEf4[ELjbFh* M!!(dR(*cFQ058d9%!3!!3!%"!*!+J!#3#A6k!!!`*!#3"!m!3X(8ba(Lr3-CV!d BSJc[)DSHdMJfajA4qM$EIEHA04+TL$!eZ[MpjCVYLbfM'iHTr"*%ZFH4N5hFfl! +TLP#&[5[LT*Pa+ha`*(SHb%9+jQ[Q[94h&r**AqjCNVDmXq'0U1l8cQ!3b$SFqV #9ke('28'r-(3B@+dE6)`pFS'bm#dI-@!M+@1HJa!D6GZS8)#-lCRl$cdE6rCTd@ j,MJClT,QYCRJC5F%,5#VkN9Zd[kre+EmJrb#NI4c@,Y4le&Hl54'J-@)hqke+`9 imD''YaU03eKI@afNb-jk`5j6+'kKkR8'`J#[%!,U3aTN1UR1D(r[5E*ScF4YpSl f-jEA-UKM&eYALY"mF0!Hr+HJZ2E[2l'p)`AE68q&9YN9Jjrf0M*5TDG!'VR6+,N lE(0hZG9QfT1F0LSjJrJ"A)bTB5jPKQbDK1"KPe!f$`M2ekCC'P%299AC[qqY[84 )Z$L(,4'iTC[#8b6hh8`Yd"VK)#bHeeYG"TTkI05%d,%S@Q31Vff%,HI!4AhefFA E*HJEPJPiA`Y-K$RZ%L*fDp)b#hIS2$N"cF8qI,)!i#`400Db%INBa-,%iaqTk*Y 8Jk%"Y$Bi2mXHSN`Kdh3ZBHpmUkf@PFcbU00APh"b6ZQ0fI`BhUipYP'LrP(i[%G 8J![hI6V0bQ9q84+!(qAQ1QYSN[amS41dPk%lA6l*V9!0r1ap+FQpkirMGP59[+f EVpk2mj@f%p1lr""Ear&*5fjk%Q+@ZJ)*B%qJUJ2B!Q6&'hfPZ6#ZqiJ-@YA`0"c L06LG8N&RThCmdF"S4"Alqe9cSZKG3H45d'h'k3Vqf"MVP&cG'rHa1-%%rSYN&3A Z(D5+I-61a'F9lDY3(XIp&N)3lq6U)6f6GBT$`kiRrV-6QZ3*PmlZIi%,iQk,5[2 cq1qZ[(9!,cirSZNS,a1e+`MAH&QKD!J+I1r$$B)-aV$`Fd9mdIA`SCpFq"k,MSj 1`@kZ2dRk%3Br9#6PY0Ikp0!DIP#5LdJ`K8@AF[`2aV5C&a06*,'arNBpSlGLj,3 C!l)iFK5X)#4q8eI3VM&LNMl6Lb$rcYCSP0kiiL#l"RV@EC[ClRJTMqB!J-J#H,' $#p#LIUIjNG&[`IZ8*S6R(0(8[$$("`LBrSc56TmApQ@#FH5qP"f*Q[Ja!qV3Kjb f3#ZYNI(E4XcGeErL5c`dZR,YCF@R5+MpaAGEcU!QEiB(khP`'E6ma0eFJ1hI!)2 L9CEeC6DQ*L'P)(Ae9jTDXbSL,BVjkcmmB%d&9I0Zp@hTi+,&Tl9l0$GmZfBU%[D KpbTc3+(Y'qJJh1X(`9YAMeameQcX[q[D)"XYDQ3-S&Qa`*,UCk#!kE)S28j"dAB G'J5GMZ"[9K@1661`SHi2e$3jl@P!jI&bl8kdQj*cU0d9FP%YcJ,!#!!1jJ#Dh'J 4i&$CpR$af)bX'1Za[$$-Lf`i0&')hH36ZGGUP$@iMLT83jk(6l,IGJ13!$frTq( YB+Ld[QA`jK`eN9j*[#dB6(rD,1jSN@-'kb0QHc)ASj3S$Y1Tb-jYe'A$H(V9B[N GfYPLNG-CX[i9VK(ahT1X8paHUS*d#mZCq2GcQEUAB(5bMXl53dHMJY@B*)6McM9 dUMm11,`cPP)L4Zp16QLTm3PTHTTrk,0K8j!!DeXD8UC[-CVI26X%F-rIX+@pm2$ mDT35TbE9&0(TihU-SXc2YIFmd,DZE2liAQQV,I&13FV'8A!BGXbCD&Nq5'P69+6 iHEa!1D%DEeR!c135aqJAfCp6k@#cYr9`94-Qjf"UphNY&*`LLaaA9Ai&IJLhq)T q6"`hB6(LK&c'9AdVbh`1pKCAP"+hGGf4`,9'4-*`SGMTh@I39,(5b15KIGr-l2$ 0q)0rHa34ZB+fEc1%`-Z-9F4)lMqJjqRL#B6jV-MBB15#T@lM%&hV5dH,Bi+ZP-m Pr*-0e+K"ZBbTfrf3!-l%!"kBl)F4G1SM`bi-b`CM9)6UNUIUq6pk4R[Jf)'hCr! Nipp0VHL[d'G4f56`H-+pNRalFlaeT+D2qr#Q1N24Fk3h#UBS!G!BHf&'[M2TE)R hJ#M@Q6`&,k4S"Z4+TJ)c!)5aM0r1j6q9'4#Sq%ppa4D5,QQM+-ar-f@9If++@!& b3EiF1ieh8(e$'Z8TXX%hp62F8c!FEd4fZX8e5E&Sb[K`ZTQ@b14T#RUCdJYKM9J baN(A,I'b'NA%!!(,0EfNV*5U)q[N[Q`1EaJVFU!)+846B)frci9Z[2kNalpE`N( ZBGc1cQLS(miJ@JiU4)D8UYA60E"L'AQ'ijAZQb*KkY5ZiQN-PEIBD3ped56bQic ad!$[R1i5XLr6rLC*H-FH$X8NY8Zpb@*XCAbpGb5cKeq3!%mPQQFCF5T1q1G@hm, q(Xke!GhP6,(,ZDe9IMP#G@X*kRlpjM)!M[kH[Ql6+YI0SZR"mEK3J#%#I@%!rlh K([X,mhRAq`B5aAK)bA%ATF%'bY"69"59K2FJ*B",C1ZJ5BGlT29VeQfccpEPJ3* 38F1ZbAZF4)*I1ah6FJlLEAeTTr-JKAfMR'-SRFAbbSQMN!#i9Km'kN86rbHVY#N I%@"3+`j1V+Z2-F@FZIaic`C'b`cCIJ(T`lGa1ekm(K[0+L!(KP&)ZMk"9c5Ek94 +Hc-ZqlrUfElXIicYH1#(P'fj0Eh0&9UL%jE3$0r6#6dCk@24J)@3!*m9(,iD"A+ E1fVNq2E#$CqBb9BkU5*4EIfl9iIFXeY'jTNGcUA%Xe2#@rc`**Z2e2hMaIqSIIe &pdZ)jJDZ#3ThC2Ji3f'A1,Z#a(8KdVY35SVm8q6#hY39$lEb,'qU&Km`(qYm,F5 HEK4&p"AfTS'r2qcRTBG1L(qJJeGi!8e28ZPq`E6Cfpmq1a+Z&AJGAh6))*T0IJb rqkc+hPa'iC18aa8kjH8AFQl0)RrjE9`A*0Bj@r4hZ)ES'TaemJRlDp0j5MTJ(N& 8mi,PmP1L[b$IIcBd'06UKCb!%[L5PkY)YP"G[J#3!0")Q*aXr+(rf0"'+1K0eeZ S&MjL#D2I%UR,$-TcXLFQYXF&fE,%38S8BX%rT4[qX0)DkLpldjrLUl`rmlZZGYk DRCZSR&KB`d5-#!A&Bh3lY4FpmZLip"P1PAG9KJiCkKHN%mef,-kiMh'V1dc$#!M %TdKeC+hcVIii@!b,1Jj!"6-D$Jr#60k9rB2@P+f6J28bPAVH@F)SGCrm%QlYa[f l0JeGfmAGIa*JbeiUj6hAPmmRKIfRKR4[BZQeakUV*b8,IhaTF*hFJ,@P(26M*hV !1JXT20Z`)I[*fAK2IY+j$,SfY,5$pc4i60&Urpm[-4j*MbGHh4iB-)A"[B6"08) 4V8feM&H`PkPM9UJD0!!hZ3cBrrQAZK(YC#A+#f*[2#dGb*i0m"qZ2TkPff!N"-I X"PAlGNH$r&1E*6G'c'hE@N4Gm")LkMR#KEkkT(AE,GFP(aQCQXNGILmR!+LUlPM F(Re$*lYB(ILpK`)4Q+&JMRDIV83NYTjY'ICY16Y$@Y4#aD1bjc"C'`cDdbS`P(f T,9,rpTq3!(Hqe6Cr1'9N%54"YqDYUqMTpaf,KXZ%HXma1hpaR'(c+E%ZPVBU'PH I5YmeZ&(P%V"SJm3Ip1U'eE!Gj44c431d&qj8UJS3ZefiXT@EG,#$ePK+1P8U@ed iBB"Ub6,T4e%fd-IHi3DCd1KRQ5"K"4GDL5ST#Pd'-2UH8Z!A*Ic@PJRV,L"AHjX Z!#9DA6$'e-p,()rU0hhMDEAp9HaaVPKQ!NYZ%HZCJ6Ke@+ZXYTPmXrS`AhmcADV 1@[8'p5(*"E!0#Qe"TP6E&4JKqbZG,T!!AGrq*5+*)@[$bblKac38U&f!Yq9NEB0 e,)($T&XZb[HIF3ILP1#)p"-RBL8JjrB0U'qLGHZ1J[S+`pTl@XpDNPSj%1`AQP$ ,@K"9H)G5$3#(KZ!#M(F@N!"m8NV1HiTC3"*q,"4dh56cBdpR5G6D3GYdkK[BjX+ (Q(T6NU(-Za8(T*NbVElHFi&@+dhSQlZR&9Z-rb@mD4cGA@ML6ma30DH+e(hC`Nl [lEHmaSe)9a@@2kqS4F!-(i8Kd2TT5U+Xm6dkUR(4L"B*p4bUVFTk+A@&cM*qT*F ,pi0DICm11SfhRJ"XP)l@e%8!Khc4jN&@q[*,p)2U'&)e9&0"(kq8E0Z,1IE&6Im 2DU(Z4bKb'pQ'f!NJD6VfTmK#6b8fPUVP4%`'J1CfZK)a)%p2hp&#!Th`,KcY2EF H6b%e"2@5#l+MI!JN218SbNPHUR&%0iiRf5@+N!$qe5p&0ImLdC`3&-,0I((md'` VENQp"&ihM)c!Q!IbSV99a-3[3pP+,[rljqq(L0N4C-#4%(Z4PM+e&)[-jRU3!%# N3!)lYZlX5Z'[Vr(I,M*bN!$0mrajEPRP0[-qp8IXl2c)`+#`cQm*VG8SZBc#,5- rbaT8Cj)h,kkFqBY*3Niq%D-2K,6Ue0FjATk(L55raPq`"**"0RFPrHM0*3DR*%C lEF#F%095Nl,GB&KMV-e3"McV*0mTl$Ae9q[-22JdqJ1a8Hm@bS5GiYUS`E"r(Gd dr5E2(-eE"Rl`VqEA!'*%ZmrZjXClYLRVPFCaqME!@lFFSp,TQYj4K#&Sc*`JA@' KHrQEmHB6$6"8alkJrj8Ur["A%hl'3Gcd@+fTE"HL28EP+`-Vp+qT-TqT2Y3V+H% ,0[pRbQ5)N!#PP[R6mhBBIhN+pZ"@kMQC"#ZUQHKIACL"A16f`9[Ah@2VHRTrFb1 0!BfdII+2ab-8i54X"Yk92#Xafq'49)Pj9l(ekVJkpN6B'j(ajm`BGh6DI$ViLiX A3c1V[10kHL1KfNh864+1bI(+L,b+,SDQ"('8jPI"G@T4SXA*-3P2RF8!Y9B$%5$ aUUeGjXaaNq-T0#3lXCjEH6F[M$'G'bP6-R-jGX['3IlXXK%#*$AeJ-USIM!$Z'2 mbII'9&I4rc`JI-'GIB3l6rGSl#ap-i!VUPcG15,lD&DNLUd,5XT4e@fQp"1-Q+$ LMjQKcY-Hb*9J8PR"YrPAmZU)HTcJhdq!iVC$1MM*L[4ZYdL!hbChN!$[qHKHrPU j(15EmlQQ'H5'Jlar"N!ie2Dj)fYj,Ni`JbaIakRpq@r8fX@Rdq%$kea@PBrCI+8 bS)Z'iP#,I&pbM4Ka%T6'R,-@IPV&e+,l[E1CFed%&SmbYJp,$%f4VLI"D9S[5$M i+L"4Glk'YIX1dQX'F8M4(d!+ASaiHCe-KYUHEbJ8D@+#1mA&EMXaaM%@PafRX", 3@H`h[YX0aVbd*S1[p&(l["hM*QKAYJ$[m$pPAa+A"fHNHfb&45fL*1-`CKhKLY- #T,kd*aKUC-X0,V,hi0VB4J-[K'M%#F6flI*m`Bm@qJ%NF$HULHkk42RV@,RKhCq *2ifY#d3LDbdraqXbZ!eDFr+[XI8(V1`lAqPQ$3pIDZ'QNb%m*qA5662lM@i9,%h lB21$`!GK0Xc`UKdUk&"46S,-'d!(3fa0cIr843$RIbEK9fBSNL#``#'d%j!!IdK Yk+9Mrk8Ubk[i1*jD!0i68)acCcrHmpLa$f#H9lN,X5ER3"eV%D%f[IS93NUdIMI pXFrD3E[41(EC!-Ec#1$Z[@)Pj(pq3LY6QUm(mIcB&GC0U+#%HBkRZPD[P(a*H3` Gi'aP4A"`p4T(cP[Xkf0`jHXpZMRQdSe0&lk+#d,KecieqlcAUGieVL&KG2U#PS' ,)l6@kUkH&YDm&UFFiX'$84EbbV#4!DNPreRQ!YqICB4-hJ-(Y`30@e#j2h)&"&M Hhl)b!+K6j%H2cd$18iUk1HMT*2GC,)A$S1TXf9!cel"9,&flJ$02bm4R94cc2JT RTQX3aR!k$p)+e4hJ15NcH66aJ'#XjE@$A0)Zq2-bq,Ga&aJ4'4hVZN93@4FT`$Q SC-3[*"904QX4k98MhXM1hijp&N5XS5d$IZa,ImFPF##h&(L,#eb4S"*ZEbi",`k 4&LiH`(l)(JHBNP$'GMhkp'A,153"bDSUb2bP*1NC$Le(8,)Yc5CRUQXEXhR,cJX B-B2P&ZJE,l5BLMMe+`Em6ajrX+E6e'#)k)%k)apNN`l8*[P,U#1P&pS4K02SkC6 T$iQr[`l[rhB2RM&@Bj-$B,CL@+`Apd)ED-VLE$JkNPLkqcF4VrI6R+JV`b8T,DR @ph'Y-bCAR2GIpF!kGhJbFqUKYLr2%&XU%qik"JGehM5BZ'K2m48!ldIR1rl6D55 RQ#+q6KP3'EfG,G!L'lXTh3[%Gh6TGE!DcDV(H5fdAC[5dNS`&C*V#T-DRC@Fd&8 p"b5ir5'HV!JND0'96KkRhlVC(l((BB*6A&b!)ALi1B"Al0LL8,h8k,%0elmYfJJ rk1YChm&G9*Q"+QQ0RU2l2!c#NI'b3e[c&bZ"#9i32N[6@jEp%&-Yjl(EB"S&lEN @`BfZd!65C+3%PK,XG8ik3&BT-CM,'9h[ja@V'I'p*H%[[,&+diKM8`,CHA"4c`K F+966VKhh(Xpm,(GeqPGL0ije*+cS)e`8N!#M6CA56Je2rE6@k!kl+$IG2efH)$p +j"CHdqjPK[!cLPZHr1lc2X'qpRSj3S@`1XHhAFDdKVGd#bAj`-VaX5Ak*Lm!I5i E'[Lre,LFMaTL[l4emjr+-K%UDJbcb%EDpURl4kN2,K+aAh8Sl%e*+PKb**2YEp, Z3GV-T''#K0'EQJk`P1PJ,F#q9GICcjRihh4q$3#G(UT1eR+Z4@+MSl`GpS*XiK! k0IDTMiBEAND-E05a5(ZSd(A[N!$a'3m(LX`!k#fJGP1CRZla8qNrm*q-3hLC6)l Bb$*8F%X(DU@2H@)8ABdpB-5p4c5+"E,SCD1eL1$D0PeA3DUP&q(%%#01JQATU(5 m&@XQICZee#A*(2`*PS`c45PLBE9AjeXjX#pBcNYUAS14NR`Hd$U2CrhTT!8Db`r @bB%G*URCG-LEhY,A215"(UqE*3)YJ+HT8mG9%hVZqb#rP9LkHr!1r[0@[9j8*%C "erBh[+kMdAA5XFM)bFKib!dRGjBEjQ3A@'3"hS-@YE,P+PkGkmBGkd22#[L%IkV C0TlSUTaYe&mDSXiEEq1*&YMYM4[,EB%d%Up+*)a)`[K$peqbrU!r5I,)Yhe*#JP @'-l@U@&IISq1+a,0P*DCG653!(3Z@SL$Q-hMiK,G5-Al`(dqc)6qVl4fh0'%dR1 #afe(qT8+*V+j(#QI19"%!G)CFSX48iFSfm+VNGj&4P(mQT,e!j!!`P*2L@8I[d! I[#N[pkkk-FJ"q0k64alANHP(iGFJ+)I(Z41M%D!%c[13!"kGLhlQ&mXGTQM0RUM R#qem(BGV!fbmLb5XdeErDQmB-DPLJ36H48!@f**J9LHV$V5kH[k6++aJ[8kKE"% *F@92$$E6l1K,aa,E3rVlULK-)B5H&,+LCp0A-K4ZPMQ$*l2m1mF8B)UFU6G#mch h$Xcf`FUZ%9TMjN["r2kD&rj&P9LQM"C$REj`!Hi*5MiZhDBll9Q`hRDffrpV"NK %L[CkIjH&[[U#I*L!m`$@`q'Pj9LEUY9BiEAfma18+qHS["31'V5mT#8+8f!H2p- X+V,V)(*C*N#"8&rhfb5'4P#PPh&l%F[(aBKq9-(!XjHeYRKJM1l-hIC'qTh-c+0 CkN9NX"UhJ4f8@$IlqVafVdf1#DqL1l+e856e3Gk6i2*mej6'4!qFH8`KTVe)Y9H H@qD2FqUd@AUaYL*Rmqk++$BIT#NN2@N$l3PGKcpF-kl!"Tf'*@ff$Sp5Z[GPEei ET$KjQFLpE"!QlfK&E5"399fjq1qr9-4Pq1iCEb)iX(2)'dAcQ%$)m(EY'jBN*2d ([k9%PqZ-rChlUifX43crf8ak6QhiEFl8&Z5#ccRP4$IdH'(r2jC&e*!!DSGp62K "5R$ffK"[K5&b2Z@QL1Kp`ZeiT!R[h1P,c%,GI1MiXD1JLVqTDa24rII@J)YY-!- SHljGR*Jpe[h`YRAT-2KHN8qechqP5b(K1)I,qN6YJhMiCj-I$c`NDd5l'j`F#a! JApb&+G5Zq'$2@qMRJ6)e!XA'3DMCqI$r*rR`&h-!BH5qrGpGMpq%p4AXhNd$mQj fj!GKTJm$B+S5$Gk@!*U#pcp&K+MVUEK0GcRR'UKKPj%2e2KDmi,iXMBeAKH4@p@ e3K'NaBN0EN*eq!eR0I(qUlK$EaYY59P0JdbDQEFj0E%UUjU2XI-'-LK%kIp,M8q k!ZELF"R`hM"iTh('@(1L@d9deC33$8"(@#6L[k'LGDPB[Z,8Kq,J[Z!Q)"MbfZk @A0[LA&Ei48@8+j52E)N2&@A`UrmShi`QadjIfeTL[Nm-34E1NkH8B&cAe4Y)X!0 c2!($4EDPeQPkrE5B&`Q$6F2NQLprD,Hp#'mT$m9S%,(i&iR9[bkckmL5Y2c)G3r #'PimrHJJHapJjM)GZIP+`@-dfmPadPc)Z'm-@@ZeGJZ"DH`'kqrbhc8I6Uq3!%d 5%XRe*PlJXMjpHY'@([[H18EFE&3)8"HF(GBkTBD89C(-eJESdlLUc6`*2L(!0[T XX!LAjFcDrAie@+V"[1J"h&B69eQm[ZHZm%[e4QHI$%"JSC!!VhaS1T+hR"$8BEC m3Q)kAY'iBl+HZ'GcmLkj-UF2pRPp"(NBM@6!0R0B#l@iSVekf24dAJ"NIl[93Ql +0j!!0mG3E"-(i2(IM-AZdfh39r,#+fT8'l)Be`&+N!#C&ZA#ar-+"[E)PY&$j`, dN!!3VYRfCamdVLQUk$%Jr8Fp%KUej@GIec5[#DTfJe'lP*!!$b69JPIm0-8V0DU cIZ581je)IMLBLJ)9pFZXQk3-ei*Bf`d0IFEZN!#&(I@dq%DPV`$1!fZAl%Hjlp* #&BC,eZi4AS23,!S'Yd!DL+dqZc,R@alCT6Y"cZDKrhISGS+VD#Dp%f(LJmem06* ka"jQiB9ak`jG)pL,mPLkJV+ZpV2R"KBAI3%Mqa#LPh3GlCmTmRrmIcY1@3+Cl4H Uih"4FTQa2DI%DE1f84X'pbT"NMLm$5-!@`FZUZq0)'I++pXMB2a!"A@fG@pXT,+ NFGF9i)"NY`6e3e1'1C!!qLBqVc9T1+&I43%FSRj,BVEE+KI9Z"IRVdk0F`$`b$1 JN!!%J2qKDc')I)b#(Z@iK!!r3BYk4q@iHIX04ZGa[)D0YDXp+ZeaCpcQd5KfG!j jk6ESdf$BCYUQG5@8rVIp$4F21a'l@[Eq%BEpRPh'9DikEDhEL8FIk3-(HZ3c%MC 8-r@Xb8haB5rL4)p'+daqiYH)J"DNffRrm-F*JhbN8Gb5I'14$3"5Qb'JU-Kd3!B FJ&Bdp*dJJF4AZBEYRI8)P)jk+#38hf3`-IDSVB!MHXY)ZjdPX,G'Tl!T(iDRHJ% TFGY#(!!'QmAJ!HA@0IE6G+UIMd'Fh4J8CJS%IE6Fl@#3!*J8'*bAhriX"F#h*GZ LTq1XVIVjk1ZLfL@+Fk(rJ'ZU')TMVJ6S33VCZkIHN!#NEXTM'Zaec-,hR-M@ECd BGK+5%"eDrKE9fA&lL4(qXMMkj6biaiBBKb5,PMHYXC!!T14jfN9hKY-fjk-T3l) rl,"#ekDdd,ll%8#hD+L-c4Lbf8*2'4[QI%!N6SArl4Kk[j0CkA5eNVX9**mI`I, h0&Fqkmc`(-J4XB+rVRJT'p6ZL$+1(@S&r35e5f@B-q86AjmapK8'`kakLEMQB8j +JNrQlM(64,+h`*HJP3-C22i(4)5#`&)Md58HS$hGZk96h5EVZ1bRQFP)"5abS+R irEYq!BL5Y#XQldZ66eFM%8DK[Pr!kPE'f$QZV!ff$Ha#Z8GAqKCR)b[cic"XlAL ZE11UL@Z1HXpb(K3iS,(EdIV@M9!F0N$KUMaLBbB`1([[V(6'I+iF%8+-jGdU&$p %qmd8VGVdK8Rc5PqHLVK%)iJ+3HqPGdEKS210Af$f&T!!AD2P1&R("jRbSir`ch" F@hZ5E9Z@Nmr[b(DpYH+XPL6aG)HJme0iM1(J3"aBFi%qMBmK91V(6N44Ir-c2bQ VfFe$jXJTCpRa%5E$ES"fMBpGP"!pfBk&C0pIirXbKV[PAeJDIKBpipaQeYI'&r! $i#5TRjh83'G`Mpb%"i4XfMT8Gaf4dUc210pB+'E@4$KmI*DLK!%*2U01Ph&X+B9 IN!"lV'0QdQ`2eYd1RjaPh40DG6X*k3IJdFk,5q!0baGC4%Q[[c8iLEh8aR1V"+$ XM5Z@Z0bN06UNFdAFGI+$EBU&"YICqV0hRqFG)&mhJmmj-1Pk2ma+4(`l2HadNH4 ,36'2,14U,3aS&!+e"6SYBX6P6DI16Y8i0$+6A6YPHZ(SQ&bH,RIP`+CM3B+H"3R Jlp!-Pj2PLE`ZTr9%MDk63IUXD&&i@qjR!el&Z+M#p[UmX!bcXckJ5FSLj9TQchi ,-PmK6ZlDEdCR5@Kj&Gl*eePTGajGeA$T'`B6Fl)JlRdF@D'MErI,lq2r6k#Li5c 4#cV(mAH16b01kL(l0mVKd--dS)dp)+#aX!I2#5QPdEXaEYV'S$%MD02K(m(AbZ1 F8&)#4`,ka9pcL[qdd!MbYL)NSe8e*4e(#e'*2j6e6qaYMhmac2eD%1M)e@rD!8X h5M)B-RkHd'fpF5mH[km6R%m8FEhS9XTTjEr45)%rXN+8NTFYi[Zk!f`M,r1EPdX 3b0JJ`a@2UPe%E!EkL8pd#GUk5bN%8UVha$dX8C6r6,cC16VNb-@Ul-%aM5UQYEk r'KN,0mm,-21#,LHMJBlMVPSLD!-dZY!)Y)T6@[B%LMI-9mc)VUCpm3ZEl[#@d[* bNUB4h83GQUB#6,jVR9k'6hSB+I4'*5NTrNKjZZT@k)XLZSl80eC`4*F2G)$L3mA 1l'9L)plZR$*leh*(PJ[4hjSI1[k@I,@aljGmF62c6ISiKq0kd$Jd$Ba@R4#'dck XMHTY,DC5`l19@%9R@JhCQUZ$Kj53!#B5+pN[1lGXH*),6T8lSEZ%8qcRSq#MSa& LK4CFH#QX1h0ZeJcX&Gp$)h%$3M4fc9!@6"[2h9Xe3I9YqPp)P2i4Yp4E6()C*T, pSZ[!V@C(XB$a06!#B*2EUEr-ShZC@E)Y5iI-8LhdK"frC(GNRFDLNHD4+hrB43% cUpeCp9fLlpcV0!V%R5i#3V&j2XS'Zi"5H*R)#k2Prle)hR-Le#lN9Vh1KEH4r%h d#55TdhQMK`[@22NaSD'f%eVJ(Y$k*!#kP@qL!03UZ0iU''UDEphEjb'aIqPi5#1 ,+'e#bJIErkXXKDa0$DE1Ai,idi%6U+BG,,rV5m)US$FrHNQHGQ,0Tlir%&kcI-$ p@a2hPXN-Jb%%ri2NQCr#CR!)K@3iP%!mp4aD('FrDda5-@*'4)VK4f#0(jd2Q*b QGZ*5L*!!0&`9HZ49#1cL`4-kFe)bhe%ID[IIXXiV#!H![Il"eX(rRVXr`Tjj9Q1 &#rj43Uf3!#J,m[(!km[5a2&'Z3`QXHF+B9-8mT@V66@HCm3Q$"fXjhG+PMqKh-$ "Dm0faem,#3f"C@jGYpe9Vj'"A#a"c8a,-`qk"EU@cZTp2SSN`f-`3'Eql&fQ6G* ,E*aA`C&eiX2R13#88YcaS'65S'L8eaTI49NEm$D5La1E2M08`h1N83C#p@)3P-c M)*(EM1'Jf0j(li3P9)QrfHD'mYTqU#IXi9cH'P($mab`UK3aIh[Y940F)H,E-'H BE4fTR3KHLq-(0aRX,"(Nl#B&fk0AHZ)VB928pcb6hj)[pX*'eX86hRAH62UjL)N LDq'BQhZ5fmP8!50&!!!cjHekS%-V$UM"Idf-'+QA%-$12)4!F%,mRJ0R)RN!C`M fA42eE!6NlmIA-lUrA(#!lKA&-GaJGbEYAH$#@%-PXqE6SpXS5f1!h6SMmT(kTj8 q2IBmDhM`NP*cCdAPmPEF8Qe2+(P2I51!KPYSXJilVY#C$YZS1"ihU`6AZ0HEDq% F9d8rR&b3!#b+JTM1ACM`q#`IH8UJ1[ar)l!QS0"EX(U*bj0a[$b"&5I3dKKRXV5 &@I+N"PX`X1lcblC*$V$RIfHkZSN$iHrkJ-kU5%[e8Pq)k%*XDH(0Ark8S[FEjMe e*l+U!Zm,JBH"Afprb566fIcL`X4aAT+3!(PeXdqi`qcDa*&bqAbAcS3q1e9VQV2 +[2U`ZASEIXE+4C%TE3U2ZId(Ak8HBd!!ECjE%L(@L4X`T`KLmdcUbZ##-Vhe`'" PK!C!9r(+Y&"+PFP[XfjBm0[I-(0[XL*Hj"a@PBXj"KDfH(fcma'JANINcDA@9MG ,hhZp5Ge+C%RKbTFM,Lc([56`Ka`lb6KM`!YeMpLh2c6[%eB12fb9E[rR8M5j-@I RYI-MXJ69GpqHi9-[PK#Q%H,[SLGB"[UZ(BCZ,bq2YI2%`AaBHl$@3(@[FYTFc@R T5A,T5)I5Y68P"f#4-Ea'dY-kj+'5r[)MEbE4aIS2+P1mj1krS&+!CFVPE`qI3e4 ccfDM(ZCYbR4Ue333I8@HAmP*CFIIhDE#L*,6k"#6l9jS@!R@r4D&j-q1UrG@JT( *[@G`mjm[qjAeZ&bmAGbj&8L4KHi9IlZ3!'@Z8T0ZkXZ+jqhNDe"dY'9XQh8F9Mr a+9RY16kec(-Z#dhC8pFd9FF8%2kmrLVb-la`PAFqKI',4%hA[(#mmbmrrSC`l$T m$Y!`#cYU#f-1!8(6BCYqNb2E*001NkherEMBBMH5b0LSfR(h02!"%G!FM-0eNGK "m'ha)9lJV%N`e1d#IBN,ci63Nrl*6U8K-dpED4Lb)Td5jlR,CmdTNk&PQ[h'Z+3 N1'AahC0rr"Y*U"QK@q+Zq[fl1$P*%&JF%QGdpliMp5rqM'CI&%8#S9mR9hS9f(q +MZCbDa*9pT!!R[PVC0-b`KVjRkf"62NTfG1'Ee45J0iSPcp1HP"I'8eH*(dDPEF 28-XUA421MG9-4V+)CeQG!VVDCjaqeXC0Cc-mp-I46I`XBc6qmB"D4$p+1eYL+mr J*,$maU&X#ci2"LrGFiU!,i1$FF&+QA(BPkR`Q'fX0U**9-"pPbmrU'AHEc8CRd9 #UE4`NRmA(kJYmhVUSr$%1MU)&CH!X64rY*jE1XQkQS%Z0S4r%ad!-m&8&SEcHUb h@f5Pf-`$ZG9H&((#mbR8JqqN6amlPjP#bSpS9e0a!J4C&'`dTPFcBFI+2Qp`'(X l5)J[EPL0[d1df,6k3&0I9c@F$INUf*fcQ55HRcqY@0DZIKXK16`$pCBML#qL[kb *H,brGbLpqGHM8*iR8+1l[8)qAKSP[&*hQ@5AT9C*JS3H4fK9P+&r'fS"&ClXI4b pi#S1lDTN[+cY'PSU0Z[p-YY[T-9EhMY0I"FX-eq2U%-)&6i'!(8"5#m+bSDKbEP !QSH`r4QhZ[Jj@CbYK4pF[,Xf1lVaY48mB&4CQI[*A2G&KLf#49HU!9ISGrf$C5, 8UV+3!)ca[(EPZM5aZ2mfR`(q,&"9Ppr8ahJ4kVC00"AmcTZjV55Jar9VH(YcqQm 3bjS2iQM"+3dfkVlL,kV&QV`h#[r++i&Z$3iVc54TTCmAFS6el"V6`[1$N!$A##Z XdQMTZ[9I,@@IV-SLLmUK91ZDc!peXd(3JkfM5[22#)9"0@A[V9pm3KVIX`k9S*P iqa$j(ZqT#"K(3Kdc%)pq`Ac&HqAr,rQD#F#VpLrFT90mdHlbP%h@pH0q`0DLj0R FaB)1+hS-e)&+)*,B-+Aae'#ZrJHBrA,lY-JFjGaM'QAAMEMr4kc-@`h6crNBE0+ @Z"JC$5!-b,G#4h+[V(9q`K6pVMPlqU9%Q@i#6R4qf23c1F6,84PJ,B*)BZHhdrd XCihd,ZF#FJ4+QVKNVI@KfJDJC&e82pbqTfG8R%[+kKZV$bKT+2jU2,6m%c[1IiP +pN2FL!VUf!XNA-VTL@E*BiApL3Ia@'Z[),(BQD''liN`*UGP!Gh*AkdmL-2&+IT E"%aEd(DS`5kh810(k+3`EBr(lU(i3'c$dqrch$l9UhMk9Cm[e$,`!Ub'd+Y@Hrp (qeYCD$dJKXi$HUX[mdJLb)CrT%QI0AVMQHCNT1P`N!#RLeUXc@GZ")A)q&BK9#i BS1jSfq#a(,G9#YcSERHP@#'mj"5FST3kipDN6#YTlaf-0`[`l&XaRH4+3FN&L,# !K#(khfQ*0eYMfb181QlT,!M,c2$2Ue24RU#YAT(kU"MURQlbcGD#0X#Tce0mFA, *kh33r@63V%VblH1+kN16K6U@[fD5E%8N%MqhUp-a,kKAJ`q6KE,1A(JAG`(5R)h 'IHjEL$-iTXc9M+Njb*C9,Kj&1#E'jJi"ar84C4#66GIS+d!p&D#Q$lSAp'B#&CC r'S`85qUm,@(+h3dX5%p0D'PI8H+*c)H)(a%$k[CMpjeb!Paa$P$N[ZAF,`5%T[5 $K-J5NpdQEGXZaCa`Kh5BRSLE'1@8+Ef'92T19Fr!BTe+r8f&AXi%mlB,9Y*,0bM LSR"3r*M@)*Rl`!YTG#UKPI8,4c'XmIA-HU,LEMZR4'QRJ`dI)D52Q'XC94CMS*e NC[[&%AUMN9#(5RCJ%IVqrZ6cU+%5kV0m0iBGaiXZU!*6U)V3FPTEK)'rV)bpb'5 ZHQF,M+4TTF9[#X*%mjE5pRPF1MfKIb0Z[mF&X`PFMePrfAhN3cFh'P))M3U('05 H6jE+@S+IHQFX+(%cTr!,eDG5ClBU+C!!6D$iE5I9pFUCp*P0TZ[3ZA+N$f4IJBa H'E"J(*0VfVp+jESBK*fCC[C(U8KPaK,m(eDj(!V"#&pYN5I0a32LQ,4T"[b@)L1 HBe'@FfheXpVq$2##4a@I@D'%BREkbCa$4[[M![KHB3@595ib8ZX`*+cc&%$l6j8 T9-T'+IFbTP@"eDqiiZk9iJhheX@J`FIMBqL(QB!cDVqMiq,'HBh+,E)ZP!3#j(- cr+8+Y*qrjL@6GLp*GBVV8M(!V[kLc`9&qe423)Sr`Ba@D88hL1-i'%@(jq@1rDl lH-+"@'*LidLKafiD+*R$K)EE3m@0-r,(9#De0rb4PJLS'Nc12L[q4YjNh@96f5Q Qd5HAiTY&N6il"'URV"BY2r5M)@5lj'YE8aS3-f'd9ik4@h1b)PVM(5jT6F-3ka[ C#(9&')V+CQVMf,e[+-+!ePCi0TIHM&60'*[h1NNm-QjZ!!YDH3r$DAl%[fAL0@b 8HfQ#aGi3T,82PqHGf"%P(YILIqJU-)e!-P985RX3!*R%6RH-Fq2A!EKpM+X%C)! bHDSZ8cZB5V1!&LFfZkUN44)D"!SMiIU+6`%!G8Q%m-*B*jJA[1`cN3TEe$1C`[0 RXKiGX!J,YT!!kq'hUCf%Lq4)p(HTc!T,,d51(KN#8[PeP"akf6#%+qdF54V8AGF a)0&R#8ie'!4dfDjG$)4bRF'c`4'iCJ3cJ81YS!SAB(I)f-ZD,GR@[QQ(KeaX4J+ ke8C'F-m$R#Acd!bT"f4(L6Aahf*-pJ$#D3`ThLFkGc*6K+RV#aQj,(bc%h!L$P5 [qAbTIH,p1MX0D"05"f-0UHMJK)2-'NE+%PIYkh5QN!$@rRIe2,$2lZjXd5SX+"9 DBc5QUpfUL@2IhQh4*411T`P"*kb66eY0Q[M'US#`5Qh[5l23PQJh+V-'M5b+lj@ Njh"!M2Q)!jDAe%DkXRF@9l0','q&[[rp+&Her@K#ZbbreICkSFh1f0aD(j!!,j6 LI&"BUhKXQA8m0`jA&e%mhESdhjbG[d2HHZVQr5iG3am'fS(`!8cC5LfbVe3j'NT SV20YM$Lc1d*3*6JZ)(LX&r"PJm($G2N#[@Y)EXSA(cEA5p@#iR1"0-imU`rSQF' hZ&G"0I!9[rV@J`fk'Dp"kj,Ub)#VLlJKk3E5(99f0iGR,jS5NBlR4Yf-qm0VFAB m5d"5BF,VA$pH!m(c3f8A*!*lQ"(!l6k2M#3QR9ccBb`cqC1N"dTB-%AEpckLMJT L!'VD0-(['E[c[*eS!&GY$ffM32T[Yj@6-FZ-05&NbZl8&#)9%'1*5AcchTek1)S ,06QK!1RMp2)eQf22@83qm%daaHE,cjq5icACl0HP4VhXNF'1R2$-K$%6r6S,LXM 1j+mSlZSUN!#pRU`XiG&4%[T2@Xk+mADlcbR%%YGSe2qM48cH9Af!EK&lIECAYq+ ZhmF[&S6@+',jU@)rf6P5GmbbLaZ2A,@4'Xl[,4dM(K2E1PR,QmUaPH1+8CLJ"3k !3a$!S&ShFM-#'#F`ML)+`%V0K[RFC8Z3!$K%&'JPIR&ZAR(RSImc!U")"bXEV$S "QHeElrrj-F%F*%SFNL@$3M6qLi,MMbL-08ZaJM`KI,8#lkm-@Eq8H+),Ar8HT8q [LeT(jm9Z3-EYQ#XBe(&![P&,B&*p81[P2M(3H4HR136r)i+dBfN63YGSY"2Ei1F @jcLR#!HcV$i&kPf$`)*@jYF1k'L2D'fHpj[6BBV3q`+*(-EUVM)e4YZmc`ab@KY kPb"hFfSC&Gqk"'YXE,%[Yikb$`N,kAZrZL0G2QNc-e9m&8`IR$'&DDcF6PTf4Yk %PV[1a)`-M+iF`h2rk(Q(,Ka()fJN(iaVc@0(!+`H!!!: \ No newline at end of file +(This file must be converted with BinHex 4.0) :%NeKBh0dEf4[ELjbFh*M,R0TG!"6593e8dP8)3#3"$Sl!*!%(896G(9QCNPd)#K M+6%j16FY-6Nj1#""E'&NC'PZ)&0jFh4PEA-X)%PZBbiX)'KdG(!k,bphN!-ZB@a KC'4TER0jFbjMEfd[8h4eCQC*G#m0#KS!"4!!!$Sl!*!$FJ!"!*!$FVJ%$D@P8Q9 cCA*fC@5PT3#PN!3"!!!q!!$J2JrCi$iVM!#3$3i'FJ#3$NeKBh0dEf4[ELjbFh* M!!((CA*cFQ058d9%!3$rN!3!N!U!!*!*U[3!!$PCGGd!!!d!$J$VG6Rf@*jGCNI GCQbcc5bDr,hb[#jk[GPJkb!rZ[eH"lDbPjrRErTpel%G1lPE@TrAfGH1pqbfCTG (0pZdFZ%Gp2M#5f1hVq2R&R+ifA@jF+r,-mVp*ZjNfEYH%mm$Z[VkVZXJAjadh6i #lFJYAH4"M`cJ5hj*`ME%*r$)3IL8mLQPlMQXe$I`6Xr*4RC&E9eiD@eiaC)QCZI 900D[@,&i8@06*V)&-aI90$AA,kQ[+feXDUbCk8F0%h2R6DRd+GIYcc*riZRi81% c1V2Y@2fe69P)e0BYV@m1Ap[FP)(NT&A0cI9e6@NFE&@GV!$aBPf9CkV#+m)ecE9 p"DC$1JS@epBYN86$UUEP6IP)V!cAV9U`V,kjRVZ9QCU@ebjYAK&'jUr`ij1,Ap$ [jLRA3ADDHGY+2Mk9iq9[@mm"%l9m%Mhpk%9S**kTIc4IkJ39`)9[%al%U5jf&Kf [ahX,hVIMhBVh&e$`QB8kCTjeV'FIiZbN#AYFYq(rhf2bfEDY6Ce9Z9"P"pFVIjA CB5MCJ([m-(kQHp#AA09YCXB6ddQFTI*8J5T8KHX,Gq#0A"jUrRNFXif1G6[+eaG M')d4MM-G2cij+LFa$T*f(+6HL30UU"UZcP$&q*f"c&"8r2-`CMFleUd[ac!N30Q -rl'&"8TG8MQP5KJ"cm3HpmLX6mck"$M$2kYDU6'j)qT89PQT'VDXi*UV2P+aE1l 9PmqYZ(Vd`T`aZE-H8SX[rrc#D@0bbdY9(Yi&BdT9!1p"aD9U++U[R2fk'VK`j+r VIAZN2SZ6q%GIr[N4#fCG&9U!QN'cpRaQjD,D1Zb$1jKeeDa90mqmMQZDAPIE,&A Z`p0H+5ll8+NDL2hNR&@RLLE9ZBq[9BJHQeERaYUD3h2(M&4CE8f6lQhE0kTic)# f6il*EIX8DJqhhGlfGH6Z3ZlTYR%M5pfR'`)GGiG+h@FE!`HfR&hKrZ6-8RGrFq$ 3R@G2G*rFmjNp,DKj*$4Mb`GQe+P"bj4km2c-K30$Fd2,eCc3R,`bP9qQPZA0R3b 8$0ScpIl*GBI+dQkEHR'&qrK&U(KZkUE*mkSDI8p1[RLLqeKcqP2A+(qjIq`T6e@ @qpZqT+FB%bM2,#pe(fqlB8C9r9N0kA[B3j@GXUGb6jflUe`Pqj5kMj9RY0f!h6d mlC0UEGY5CYSfPjFSIeQTZfY2aCjjFb[B%48CI49I`!C(c5K9@FZ'MFN"'JH9C`+ ,"@0bJFBXS(85d6LQ&0KT!PUZ`rXC3FYA,RNKVbZ*`FV'4@Z8'KJijrK$9"iHNf[ 3"d5'jL331')"%2G*c*Z6`'$j4HVFYNe5XpA-RBZjFqhFj)0CUck[Tjj@Zfcj#V` "ImX*Rp@9NeI80e'6+9AT@cqXBF5d%6-'cK`iIARflEiEr4AqN!!rP"j)cmT+amq hrV5Mj9hP2F9ZX9Y2+H+XFmUGmPKaV,!V%cq9$3'!16K-r[U"$3-['6KMC-j)rl6 MFqMkiU2&X@,faa5rRX1hA[q#0jjcbcVRR&Z#0pS+cT!!YN2r6YXhiEPeMZZZFbB mGpSq@i8'QIJTp5Cl'Fd9`1qrD#jGPHVBRj@#0*@f(T0fT"h&H`GbI81`hVpH9Kr !Zm*r[,jIdhd&$f*Um04$V9&)Z'#GH3f)Kb1HDQ,Q8FGkV8P5cdN,YUkImUlLSi8 GQGJj[BahR[+Z3#!VP)kGriIkrlcej&2SZM[5+V(a*l$aX@EM[qhhQ'cpRDdPYZk 6hdN*X2i2p6+hYrAre[HN"G[m*`crIIhIEI-rp4h`Ni@3!0+8(,kp8#!p!kTQVB, QHB4+&!SdDca8lJ939&#NJEa+j9Z40@PHJfqKmYeS0C%E+aUTULkE25CRdV,b!@f YCF8AU4P9bp,rXV)Zkr`E'K4kIDa""G*8K3T"XGhBhmPdi86Rje8PjaYjF4@f8+I Ij4H&#Vljmd3f%2MQcbqZG2FJ+fq[hQ5PISVl,,,bpZT0P[8QH`MC4'pQdle*QFf 3!1`K[)piIBrJrDEA&aP9T$-k1a*GcLpeMedfHf+2@MeY6YXYCF8I8M-Qek@[9JY RQ2fRFIplTPkNG,iX6I5Xe)`R+[)HR&+ApH#8TF2Q6PjBp*H9pIjjNmd%@3p9VRJ !9IHG[k("ChAXCj[KrX'4E+bY@kEm[Km$`mkd9iDAdHNi(aJ'PJXZ'+8bEVpDqD& M"iAQ3(YR$30bbdBTrhc-AMC`6jdD5*@0,6TPk4p"6ANDIBcid4N+1MT[@@K-,MC AkMTY0r`CHY4RP&rNrVKY!p!pJ-LQkb'E[3&EZB"EZB@kHMiFcFRK&5[!0YM+I@3 @E)@X8M3+RJQa"KkCBTAe&E2EpS8@R$h023,pI-4c1,j,G8fP[e6jcc"1al0Yij) 1ajXMi(!FZ'YdUA[J`$fPD[q'rDeJV)b9bPI(kFk'eq!q"EjkDXX(cN*deYbRMUK "&mpeRl`BQ(!I3*6Zl[[Bc4q[2(Z'Hpp&kD8jm&fL-qVF*jGGHF9XE2h*E@H%&Sc )QV&KfBc3C*NR`qe#P12'X)XZZKAPTl5e-3Fm3r[IKJ`R2hHN#RbaSXcA-Hh)Y$d 6$a$RdQ9-fVCLi2Q-[(09GTPD1ZLLY,Q63c2fr+JdUbcpYXR`@U),-qC1rV2+PIY R9+f%amPC'J*lPLXr(*3(2PJ"*+1LE+6Npb'I)APi*2GM0r8jc(i3'AJQf-hpekL -&89M"Q#6aq"HC3"Gp`(Eqi$X`2%jC+lIG#jeSf-bdIRVYV2Z*-JDYI#-'CA`BQb [Le3D12#aDA23VilpcTi(c&`$T-95-mqV`,CljM*b@*Fe%Q34A*D993T&dlfX!'K j$(1dJNZA,X`cfCZCCDjYmk4PECZ)U,DY`K%j5c2%%cScJCMqV4UA*PFmQNIJdC! !DDk6L$l0GhrP$idX,bpAJD9,bmT5EaAScbpG+Qi1fAFBq!eF0iJ+bV#`pKdRcDY BPS0h#1rc&ik&Gj9@RLj1BQlaJ%R[Rr93l'PkKJ[TDL8V)CKmU(f)YCTM6hL,H1M 33,pQlY!bG&eXA8aRV2V6eB8GK4eTkee(rp,@-fmkD)HQXP#9lDJ)9B9D!hIFGXG YVB'U8%@Sl'MK"6"Z#p$"AkM+GkcV@0IK(Y8rjXU6p84DBSk+d(UCBhf!ZImlKrl plcN#mkGF2RrQ@8YQUEDbFbii4c#6"dY'S+(9-D1$bHGi'JApkB-(Fh-pYa`THCL 40j0$KJa4+[(QmarcqrEY8bVajR-LRhV8$Ad2YR!mVAE`55Pr+8J9RFL[9cI+Sl* d[0l%,8&k&VjJ-"M`i4F-RJNEN6m6RfRMmmV5q$[2Lr96GTPqe!!*jkJ--+Bm5r( !T'H"a%T9P-IS3biXlLKmHCRpJDpLklV@U[+1iJjG[dlE([C*eSXCFpPP&IjYfpE RA(EC`J&EEjifkSV`hB(ViASUp88B0U'!f',TCf@&3S(jJ`EPM"`jmUcjJmiD&4S C'(4*B0$)jY'Mcamjk)*4(lYJd2#aJdD2qZJ[RRjUjCAIIl$kp@X@A[cXdeY+Yhp rr@RBef"FS46XJ4'fFTrb2rYGjDYIVY5cHe3Dh,*-eqf!DhDdf)95G0dBhYcDrhR (m1l#qbMQk#Krh9eIMNecfi9(3GpZ$$2(P-rYJRa#J6RE1%&L(VICjphbC)0IPRX 0IPeZ'[bfA$F!Rrcf-H9[1ekrVlF0N[A(iprAfm,Iejr1KrPhip-M[k[rIIq6!G6 [jF2mZc%5*dQSrr@$0VIqrF-aIPIqp`d-%b6bleDRkRre2'kHApIrp`EHmrX'[j[ JGb2f9iVKV"@FU$Sq4Y'*ZZX[P`E*FQQ3!%Uc36p("b!*lE"D(5B*fFZE0h*pHEc IcN0*+T9i8eRq5ai+9+R%QiVdIq46Mp'[pTPc2)qPpjIVr*a%@ZH6%%hSA[03%+I bE#"2UN%UEaSNKZJ[9m2&p`!qUqEL2Sl2qZQ6ClfAL4rm"HZBbC`iCmiP'XCA$jj rA8-i9,md90-BAY3FAK*D@VXLh$4reX5C3bHcSVlaliT2RELUZAlPSZEDQN8V9P` ADQUSV3[9V'TXUQqXR(6&r1#XqZE3ULCBeD'Dj8YU'dGqB0+NkI-,*pDJ"HB)068 [DYBeqCAK&BZZ#dh'3@[pLM!VXZD'Qm+0Um0,Q-bG[DBZ96bXXVlZrFfK@Pc8e5j D8I[*F1M59E8eep#pBhP`GN1i6LmTe,4m8D-H)RrZSYUQF'MHp+PcTXqC`SULkR" M%kjU3b0ahiYjDTB[UPX@rJ!AQcYVeFTjUjBZVDd*0mfH2'[qJ&&m,TNhIh+'P&h ,*S8#QUA!4T0A-p$#TEr-kcm&D"XeX+%a[$6F'+kV#BG@kad3,d1Rec@('aIKdRG e1,4S+A+KTTV'fSCQ&KGJ@BYa,4DUAGP3hbJP@E-EQQYAB[e-"Ll$dI"56V8%`&T 5@mqL[-V`iPA,3JfiCJi,*)Gp1"aZ#+h"`A$pQK!fAeIIZ(,4LP$ifPUCk06MjH( '4Uc49JqI93r-*9C8Mr(4C!N`@,q-$BV3B&(MXTSa#&D(`LYAV3!ek0d1QEf#HeP 8Yf44ia*-9"1@jNffE90YFrMXKZZDPf1JTP80GX8JTk%q1I"-1kHd,0qlaJiKLk+ F14e52&4Sl"qb3p9(e'VIcIkPe[X$$Tp5US`(TJ0ehRh&l96IpRA[m2[H`KeihMa pelA,lrr9I6%VqGcTHe-9A6"TqXa%hG6'QaU@Ih[aeGlY-A[GIVdfD'aI2SPjXY" ,V[1mCmE$-!PjkfU[I4K"lrNCIq8V0*aF&kbIcTK#a9bJCl!,-M#M4"pQ-8DrE-E SPm-BrA*P'VLLR!EpmKQMSS!aqX()qXTAd#r!'2f#-MA``UR4la6'k$H)-5S(-dD r)XES0d5f!qaa1qKh+Q2d'mBBr8jMM!l$'D2IkE*&TFlJ&[''L8DPUd+-dHmpM0( [6-ESp&jcEhm@YihFqaLMh`M'k2GqaZJhNM(kI8"3SG3SSJ+C%U1I4c0#[l-CSem TBr3EBp4e'D[4lac'U$LA-ITKPi+2XBc4Eja-SG6jR!,p2XJBr5jJM-S,'D2IH-E S0d'Q9HSL6SYq&c0'[`ma4VpbaZK3`4Mp*XT@m-N!EJAp*M2'Zj)aqNeKM(j9M0& TU[%'TR&lk$HG-@TQ-%Dr$c0'[dXBSpp-f6,1GlKPC'Bc4VmjM0([8XES0jFaqXd 60#Jehe`5AXBB&G@-dHmMM0$[FXESGi98+A8PUp$[Sic4lf1-8IPaaZKh&@2d@b$ GP9V)lZLhL$(k,@D-IM@-d@%*Br3,bj5!!+G%[f@-d@mjBlaV'D2IeBc4k4VC"Mi !`@fJhdV'k&I(',AeM0'[J6(kI8+fKJm`F'[)0$&'[fE'k!Ij*IKBc4MpeXKfPEU @fd@rkaLMiT1-dHp6M0&[,@2dql5J3+Nr)JV3EaeMp&[2'*8Y4Z9Ic`Mp0KM0raP QdHm'aZMAbKMp0M*'KaXCSpp0dP@TQpN9r6l,'2eZBBaqYc,'qh1-dHRcaP'iMG1 KhaFBSpmA'D2IPaLMaamc4Vpr)eY3kX[F!M*r`KMpEQH-IS*Hp2XUBr6lQQa,U8h F&[TYCSb+Vc0'[cXBSpm@aZKhTfa9UDhF+[VGa4MplQD-bQm`4Vpl'+2I[E*pTGU iII6l*Q2dfmBBrEl&'"hDMG'cA5+PlQ1%IYpQM(lh-dDr2f@-IJmBXqJldNfTlfU &T[iYBr6l-mESpqH-dHp"aZJ&jdA`m4#R3ZClfXG6ImNBrEl2'2dHCSaq2j!!kA' a`HR4EbGM92b3!$(krBJaqZ%l')+2[jBY+I8)Yi4qMc*'[ef-8IPMic6KXPA`mHp NQaL5fd5rImmBrAl#'2ef-dB(h$S)-Tl3&TPk8Y5bUhkUR8J&(5f6riefSY66M0& Tlj%MJSppccm[m62I`mEa2,YYQm3r-rENFfY`G)ARHA(6P0TI+9IXkJ8DHRJkM-( iSP)bd3'diN5i*j!!L3la5aZ)Ak*eLIKPT@5L9fLp)Ai94d@Fk$9Y%5Tmq88QqMQ (`N4r+m-rVhl"'"2*eM$4kpSV96bYi%4[D$02AhGM)VJ%-Y%alAbUb"&j924jH96 Rpq44AG[N8Gh'81eC)irU03jLV&)H&6p6(Z@BBiA8ef5XCk$M["CArd6`Rk`a'rU HkcrX((DFp0IL6m4IFaaQpk8k92cB,A1HD,rhRLhhV0f#hqUeH*hiXA$,fR[Z[1H HHq2EAh-1(dj0-1-RlRKRqch5qUYV2leklCfV%ApelCE9@fcdDBPPPNrV8+)Ypfa riYMVU9PQ2q'@1rHLV4j"ppR#SEkf&Ur9RdB"NJJicjeQ$*P(clAfh[JEUB(Qrp5 Yh)l9b%['B$rdiNCN5X`LXl*)$m0kVQRYkM[e!PG[fAli@'UUMrb01c8Z3mKJHMU Z4cDR#f6c!JQp*k##fd2CRHb"0#IFXMf5'ZbMHpd21l)Bh9Ff*""!#"5a!L(V[JC BF@'b8J(,9l&If55`qM9CT01C'ZkUIHkXHe(0GA"6H'%H$5+ZbUaX#`BNp-c8()6 $DiKa3)`T9@YIlHK1cAIpXqkP6V+RM'T`KCeM8+i-mjRp#[EdN!"h-UpaM*BDcV+ 'HireB,aIZ2Qp,6f*AhbEQf%+-Nc&MAS(66pcjcNDaaV%J)RH,NP)"K58D13,*Q9 6h!1aaF9J4i`XrC!!EcTkC3ZU-k*8E"[LAY@V9+mCHX0clR`((@8f1bplJl3-dqL )ih#rh+bXefaC1-&3'#N8E#Y8BDH1+a@"XFdhTqq++eq%bP-+c")qqEcl%8GM1B% LM%83D'`4J-)AKM+`I!-*Md3&#f3pD3Q@3-Zepak1pfdrV9-Q6`+JGEpl1C!!cf9 DX-Rm-LDR&FUcM+YVG#XX3aL&T'!C6e!MZ'$q(Mfm'CT[Ii4Em(9'BPlDl1'krHj X3rCXEEKFVj8l0p5XCdX"PEm%GJeN0*+FY4PD($cKr!-(I2B&pq-J9a%S!J!K6F' hCRe0JfBm9"[LZ91[b,#QU#20aBCa$9%IGXh`MXV'r2Ve'bjBfq&H&6HL`#SG)8, 0ESB-0"imr@,PP-JhB3D$KUq+QJ!4Q)e"C&%0r3B+0f*k,Af%"D`HXTMME&D&k%U $'8XB)T1dFY#YG'!@KqaV59VXN3eNF"0i'6k`QeMhSTZq24JFPqX%dmEP"XI&JiU TA*3i`8a8"(2("A2M`3aQT1L8-8&85*f%I)hV$HES2!-d84M%Yb""5h%e3-C23-% #mIS$EN'($$3Z*jJE3'+-%b`DTkIK,c!QL+YY4$RM!Ni`C`b,d#E!ABa!fl'jJA& "[()$ZF&5YX)5T%83mf!C1F'Jd`I&VPiFeXXf,!KZ11!1hil@TE+'B1lBB#!ABc( #C!&Za*ml$LJBK1fJ$)9B$eI#L9(p(PN94TEeBb&X`PLh3l4"Vi"mc186MjUMNUb !0CbhJ#X)F+RBI%k@,q,2dK"$@Zm$!k1H*B3#0T%E#lkA6HfbX"AXR%ML6YLF'`J #eQUFC8JLdSSPN3N*42V111LqV`1$%!f#Kjb)S-8Rm`'CHKpBEcal80EC@GRbPYp Sj,+cXhUc"f4PSqKX*cYV01[C)TlY"Y)&iqV`cpf"6Va2VlEdaTbAh$bRh9EeYMZ (h&bR[8@b6-IGYebMC8Xk99C%G8ZZph9hB$c@2da,,(l),Bc(qST+%VK(brEqBJl iZMZmVkE[KrVR0%`I2ZMkYa01Q3!*&TS$KJhi,86"9U#MB%kAbSKJ92bk"H9TR$b Q6Q&*mZ82!-TUqqYZHRYeCc6bZ[*hpX32ZrlfkQJ82&dG1qbQ1GfG5+C(ZRTG3%" &SZL,hPe[ZRkRTr1)mTGdXf@mZq3A9$TN&*4LJYFaG9H,mkEl%NETkYf[phIr36G [!A!-DK#!CfQ4@*L0GC*8`#3X(-XbH-+4jj3r+cIJfp$aTZYVli6,M'Dq5-rIZUV RU2*e1dHiKJK5RA&ZakbDr)+$rXkhd$4+`MP'b-V1ZjeIZ,lH#(aU6'*e'ITd4`i VAdR[@biFEScKkiThD%Ck(cF'G*(RbBfLFZ-U%-N'$C%@*4`Ec,(%"XPMT+rX`U` %`,!9N!#%d4cIJJj-hBXe*33M@U0"LA28pF8K9*+&@(T*1mTDSMf%YLA[L+r&LEK 30mcV2Tb`+q+Q1e`$5-FdLf,Qq&'8!4CB3&iI$fKj'#(#ZYmZ"FJL`-(![V,%8,% A0E"h(h6c"F-'f+5H!+",VYD+)6HB(HR8#-!e!2&*h*i(kdke@`H&N!$A1-+k!2P H3G+EK,aNMi+*1,YIG6P4`dEX"jVVM")ej$Y1eSQ@l@JCLa)QZ%B`d)LK5kp!P$Y !Df!&0``*D+VU!hUK,R"253AK39V0cHa9Q@L46E%(qL8M3'2PF$miBV$qMqb,#l! jdK@Z%SNZTd66-0IXkh51!@mHB@#4[5L)Hqc44A,UjSi0J@MJULk8G0NKHe@1KmB 5$*3H*b!Sl[6S!L4TfpYTTf9I@`eXp%4jNbN9I!-T23FemIN1ZIP1-&G60I9>F k*BI58S5R9VQ@E5hj(G(,dV*Ej,k+%URG&KmTPX3U@J3Ah&SmZFY)($$9C9UdBKf 4@+IVcGC0'k'cde@pGKB#(rfk`5DJ6MdMHdG&AD*6*kGK*F!ALAHk60QYarA@(%1 ANIC$'J+A(R,9GLHB,a3'[!G,FIL+EZR8a@#",0(,i)&!*`5"mJ9bc!q[E'rG'#Y 6&cQ"0)6ChF4f0GDTPj6H#B(A(SqVGQbch6!4H#-+&'4`VhUG[Ef1[lFD!X$A"H0 BGAY,8pfaH'ClV+I,pR@kA#HHKSP%mXI&FR(LIK4dJ@+Lh564pRKl0dX-8B%YSkk raq,6ha0[Mf%CmELe#[38ADjUdEM'#&ea[E#ikSPES3&-JR!%RK%p!'D-BBB5$5R P3KY[$i"8Y&DJZq+,3eeN3894eX0I%'SC%p$)cmHR$X$#ii!b[#'*#V+3!%31c4" N!IaBQ5`'@U@Nr3eAaEZe#X)UBZ!BAk5h'fc5CH$B!J`i8HU$RLih,3C@)1QrUDd L-&,X64!XHi&9S(c),FL@#(qL"3R,H"BF+miDD!K#Kh6@j5E0i%K%3lbAF1KjbHM *3fl46E4`)#JK,1LMM-[1bXV1)D'S3$bBV5f6B)lPTS*-X9,S+#(-0X4+`2P%Lf5 J(k4)YS!5)%c$Za1!k"CB(i@'&hld4F!,eU`Q3!!DRp-0e%5FEPI&0"'`X+F6#Mk L"QMT#9dYXXS)jEL3!#LQL'1l@L*LZf!@HTf'*`!5XV$KK"LQ`jKD[U#bac1'X59 4VX#XTQ+XiQ80NrFFFM-AF0pBHc`iM'+,3TYU%T!!JI0"SU$m$QJ5KAM`d4)"&-B k`I4FI&48J%,iN!!Xd&!BR)6"jAA6""0GpEUCRMS"QiSE23R&P1k8'-j!(HK&!!R "j1Y45-!Vd'i@QJ%BUX8M@p#'&L@kYFraNLJ%Yk#&i3qS0Npq!"TT4XP%ZdL9RSB N2P+fQBB)UE3!ASU3!#0iNiDE5"*b+S$4$453!$'Mj&$LU*3UVM4iGK61!TP$4$N mef`p&j8U&ah&b0f[DpB`3V'(qqGZ+%1`rhB@GK,@4Rj98ha$eA#2hF5&YM[Sr5- VG)UeJM&-[fldYY,0N!#&eUQa(KI`d6aK*`C@Zl6@)4!39aZ11)8F`Ff#)jcJDB3 rV91K#"M&a!65Y($&R0&[0q,#Li`&&@M#Td9DMmSA86%@+Um89Qqf'4aVaj*JcDQ i!ApeYqZ[0[S'NPk`%Y8e'Z*DQl'XqUf%T%K5U(#$dCe%8!rfU6@&1!9DCJ%%lBD 5K$!Y*A@,9VG8B'R6Q$i*iKa+9X#[L-B,$F(J"2SB48'ULh&$8$*N,1$4*BSl8k4 Y!)DfIfb`X-FBCCQSk3fQSaN*-je-@Ad%VQB[h-M%bk"(P@#4BNTS(bN0hr56A@1 2e8310'XR&8*2V&Ve%!`'B!#'2bU+(V!`*L9$3%T9kj5K6A0IBPL4!1P0-U)4B,e D!)&Fc#kX!$2[%N1F-(F(KbF-'6pN3Y'%)4*,J*mcS@$mK2&)mSHLJ,6ZKBCJ0Aj &D$@K%*`k1*00*l!ELUJfj'@dC"bZkDZ%-MFTp'Hj'k#&c5c3%%r%d%Dd[FYcarA L54T4[6LQ`Cfb@`fZ(UZbF%p!c"%2RDcap"@U3&BJ94K*PMHMKL,mKp`4#iU`6Ub IDbl5bdDk#$QpD&eGT*RR&0m3334@M3MhbaUUkC&-9-5'T,2pHGfbI##k*5,BXqD +PJ+#"&j"p,i-E'ZGiiZ#d9+A#9bjD5dmQA$PX(K0*5J9`@"EF[-*TK$CS-8#YTp 8$9SN%T!!"J0$$VR["`F)!4M`bi)Yc,PSc3i"S#h)16*44*L`eB4#cHIj@4Sk!U- *S%q&D6&QYELTG[e*MdN,KM4V%HTlQ*GGIlIRb+%6K&L2!%-`$9LNdE`5d)-(L'P N"")*$H@(R)80Bii-HY`dMS)aVD`dFJk`-+LbbY*`X5A((L%(BS#V(JFN#$+%(64 IFZ&"C-EUe4D!"0K+NfDK995N"S1P)8@&QJ+eI`PAKZ$K'fIJ59l`1G8X-QcGhCZ k+m1fK"l-YX566KLCK)*'qh&501J6@[3i!"M3[#$L!"LaT$$iN!#E(HEQ"IM1N!$ K@&j3&Mk1!IB22!K6i,Z1D"b)BZ0SS*Y3"265"e4C%e!Pq%&R)395)bbN1*@KC85 *48Tk+N'ri9Lp4'Y'%%T*'4Fb5*J`@KKJ,Q%$,9(HS8KY00JE"k,"3`$`i1ZKA`S SX)%95If5dP##Ei"(#41+X'P`(H![B#$N08#d2KK[@&(!VdN8HN%$!)T*NmQ%3%S aDEUN&581lCXHj3Xa@&&j8*1B43iSXM2Z'-9Nf8b!NC,CR)HJ-2,C)dRT*[4JR)B HckX#(M`Eji4XbS(IiV'$PNl1N!""XRf2)L9C**i8MMQL5$Y$4$X'S4Vej!986B) #D3XK39UNAaIhVP6LlCd`F!dj'EQJl`"k$d$Ki,BQfZjkGf9'(aLDa1SeMHLV0d& !MdNDG[4F"[#M@6aJ-!!5fYJjfQJJPUchC$5X93RGS)5`L"l$L!*YLhQY%BD-*5$ 'p[$ZL*E#K!!%Xfi!Q3"E#YT*0+@`![5M6%R9+UiU4hG-N48*Z$E"q8E+BZ8UBpk jS4CC3J3LRB3IFDc9Tj6B5TmU#5QD6BYdP+CD0eZU)3#Nc0*!qL(h!`Y%eQL0C0K !rlM4XAVr%iU+"-"qRbB"kJ6`RNBTEhZdEN*Y'VUL6VZh9!VdKA3I5d1'"d8S'#@ *4VdG&&1iSSQd'+PNh#BjHM)`H9XM'*ANH@j#"%DN'$Dd8M&Uc)2%b"B&"5#!Qc` p$&dJ1TLk!*%'J"!#H-iBqCQHM%5Yd36j@938&N@D#VPpm3pMd0,L3QVQ)`pba5+ $Heq`TehfD%0%36I0ZU5l*%iF&`p50&3L%1MhPV3B6j!!BNSQLH``-M0P+9MTD!N 4CZ`)l5G`bG4%@KPUDD#A6N#-+i*'NQ-UE5FB'dU,!k*%pk6Z$P)ED1Z0YUb&-2& S%#TdB2D+#cYrHc6bKYa'(6A55ABU10!@8X+`pi54C0qf9,3LJ%E8@2"&4#KkPfc FJD9Q3`J$$lPRhS308bQ$$&+Z!6F+P,1@E%IYV0NYS#Q3!*A`R5J*Ze84j*%&$'9 "bPQ$diV0#3')1Y"@Jbr5*a@IKhi3@pdl-q$j8!Nq2*'bkch&4#dJ&'RpUJ4&bU& (bR,X0jTSh@)PKQe50'f&Xb**DT9JD0)`JPE(@P%c(MmHKbh@6*!!c4GL6l35K!q dpc#K+-8(iKc'3C&q"!Q26FL-aakpce&5kdXa+SXScZ(k2360C3P6rPF'`MXkbIK *#FPXAED%HjPNK60ZmZ`elC*T1)KY3,I4'C*1#)KQjPdPKG%id`c[3Nplj'G"@ZJ k+JJHVPLpi1NEXN+5()f18,dri`@3!&kqG-8j@Emr$beKIB68qBCRXZNY'2eXI6[ #3U0)e+-pK[#SdXaNI!3V&5+`(JdV*,@M8+5RKiN5@Sp4dS+f&841L@i'*l$K@(3 ,%"pD[D5!D$EKq8SDP,h2b+@#CNfp)Tc)(Mr@q+95X*V4+ZHhc6@VR+eL6&#PTB) LN!$KGR#EeAY&p!1d4d$P5)dJ4KX3RK$hXQ[+!GCUmG`M$SV(!m++9TkCQBhcBH5 Mk!9K!bX-pm&Xd8ejKA(mMNH%PYN!U)",lK8()@Q$("F(EjYV[%E3qXYk+BQMM42 +NBEV68C(3`D*6f#%SEC8V,8d!9H'KI!$[E-&Y!3,&QNDKZGS9!+")VX3Dkf,p!J `'#m,Gdp8#%BVlE85caaViL5Vrpa,C")D'MXqS4-%liR$$%X*eP$6I#bb%5D$FA0 %YXU858YYa2la&DH2VaKI-A`LSZ%9&89m64`rXDKL!Rl$*e5-RcKmr2$a@PS9C&B 8S3pVLbV'$bBrCTVd4$3C6ae0dI+Z,M$Sp`cRTh&2cIY!@DcXhAka`YiKb+j%(*k 3!!2fQ*!!HL"KQ(SkbD"G9%'#R4+0qNMDB#([N!!li$X9&9899I%+2b+RSULUSQ+ U9-M2"K8*FbFr8iSQkZ"#MajY*m49&bB@N!$B)p`qb%C2eiK3d%U'&bSY[5A+HFh epdB-R$40#YDe'@19J[(KY53rS4rl$#C2*afr4V##`@c*5ZG6$lRqla!%SAfbBN3 6%CLp5X*N,ib#&D-D"4SLJr@h$VMI%YZ-dH#89%S+(*+$i8BHeaKZY0!fei[YZ!H -DF43"(9Ci@5-Ch%F$$#0iqB*)%kJ"C5j1E5D'KHbePHcKJ+1Pl5SX+l@1eF+-#$ IkfK)*,FZSF@+,0bTd2[*ckc56D6m3NdlqCP6,Dj3QM4I9BPQ@QYR#!()#Zd"B1, Z6Da#(!4LQ8EX%"AfD!ARRE*HkmAM5Nb`RcTH5YblHBj6iL)5eiLTmp$qSi3c$VP C,eJkF#V5Q(%U"ZMY1aAj"JmXpeJVdkR)UDSi8hFG$&NbJ&6*GPQD1")'JEBKVH) a5Y)S*@jHReYapc`-46-r)-2H[0R(m4k'%28Jjfj*EJ!ZNL3$TV!L'M-PkF$`Tle 85!-rDG`)6eJGqrBY6p*j'3TTeC'NKVdH2D3)Jlpb$IK!9+FT*U!P[(9R3'0-V+J LaY!IDX)iF8EL@SrF%#JAC3K8lPhd95e1B18B'EH41(!h`VZGd%LHIQP)f8Y!I65 0X`KmIZ3%G4+D#AieHYXHXZ1B1ARipFlY!UpmmMZ%'iJ*MAm),Lbim%a@BF[$M@+ 3!,0qDLa0Ne#RJf%6m1-UQFqBYSL8%4DH0aIA"eV@SM5#%9Hpq[+0dKS(,qdP`Ri i,6FLfPI##fe`U3a5NVa*apde%55)%h9KEl$efBMP4N1UeV)N33-mSN)CBfE2PRM RVZ&dh0J3$d+'&!a!Ke14868Fk'FQck*JX-%GT3'`%8F2)Bd,VG&[Q%ABaKMp[2$ 4qTV+JTDP*KGiYh+2RcM+m6XYiKb!+h$E3%-P#KdJac"b30D5XM'4%eY'N`'S`qK ph2llRDMj`)6p))6q%%$F(J"jPl9LfZ!BhYUJ[cai5$rS+U$J$'jeEi@)#%fUN!$ B#@N1%S'6QkQPPUCEkSc-L#%4%5T9Jlec1-+#RdH)(+2&&J89D0)RdrG@+c#%pV* kX"4B9[jSCh@X@kYX-KmmEE%MG%0Fer,V"[a1`6'j5$CI2,(I9d'2cNLeFEl*d2J !!T&%%b*5EAe!IX2K0fF1cN&h8,KI3d!Eb0kKZ)3mGDf)#NIXA'K`$ai9iP(Sp@H I)l*dZ&#[-X)#U1qb*JNq(ZYa*lCBc6afe)N2RFKP[[jDM$(i)c$ideT3JCY#E&$ 6S[l-MjDfad3P[+U9Q5"$Q)G,Ca[aK$ZpZhYFXlplj6$LS$[JDV2`dce[34EX38+ JIb%8Eb&"EeJ!p%LJLEdYHGRp[ST"KQQd9iIYkkr%k+9jekp#P2*0&'Ee&3bq$m, ['a&)L3qRJ#*3Lbm&bBQqYA[-4f1XB*,h@r`'M6@pl,'6*bG6Aih4F)$cf2re(mc @%Zd6k)R,"aA&GlbZeZL`+2(F"QDKT$3M$%r`!B5Q%HrTj%M$dpQJ6L-m@-KlN!# B-%88RlXK#q!6aP%B"h(Mba-$@')Ak-$A'mAhKZ8E10e(a-#`"bci@&#k3aPZcJh e%R"6L%m'mC-BqT0"P*Vi+NSX'RQCR`ZaGh8*rI61[82Z36F30VZefprV9!5Y4+$ T)X+cA,aFS3,$%4-[M+Mmd5RY0&K6)TEVb,Fe8Yr3XGqi)6HpjDE61M)A)9K(T1H )f@qL&C!!LL-a--JaEF6KDd1p)$r2CZ*AZR!pK5mQbGI$l&GRm$BIKh+-dNLmHEH 1VaDpI6"TKABK51"c54r'Ji44#TjZ'Zr*V3)DHDE(ANLRA[2K+3'+lMkmiN)jrG" UQar4+Z'hR"*H&6m9CN`X(Vk8Y2qF@lA18eImjl"6))q5RbhLKm,LFLLKbB"Y1f0 rkfBjmNdG3lPbqf#N!aV'j,BXm3NKIk5EK*a`ZpmjJI!GG!IIG!)14Nd8bY)TYBe P"4M5H4&e40%SbUPEh$c$+pE0djEQ'fjqH`ZqiqB400H#A86a"CKBG3Q`lNEm*G@ a9pd"$[0B'Fj0Sp'ZPVMV[JEKf99#P`kD-LhHdbR,eI")Md3lZpYIe9med0pC-[, J6E"-,dE4)1$RXD)Pq1EE'mQEU"-#X[ZJ1qb"Y`&JZ)!8RdPP$B'Y(3GECddDF+N !aAZ*BH%AUi,b(2jI448r@iFrIh2B$HLrE*4maAATcqR-i,dI@'!K2+1AGD9Tm#U `Eh[q6*VjNBmM%f0,2B2TV@X2ScBZ'3jfc(D*)DQrr+2U$lU,pR[Fa[8C*p*U!S[ rK$f(PeB-9-R*)L1m+5$l!24mhkHI-A!D,8#R!V*#pbGe$1G)#GhKd6'cSNSeP%C BkDE,V*T*mILq*IK)AR,J19$((6@e06@eX4T9'kraepBXk8)QLRGh66S+Pp3k0HT UTfE!%QR6)fd8Lq6G@k2fmcZZhN3V$lJIhlLiYR9ac5fe$'YE%5a[ABkBZF9)km3 YLjPE[R'j9+#4lVBF99i6VjRZc2"E(AhMAR2!(AYVN!-0Tp`SVpEDMDdeNN)0#R9 LmFD0$"C,`H+0bcFkVENX4bqTGPS,X!08YTSjDTe@IfhVaYMaB6mD@ejc+r[)HM! -ZqXA"M3*[BaD6,Pmiq*EN9[1UERSj9,,YGCZ"!*U%1%&X'!A'c[kKVhkJ([*!l) 0019UEX929XU*d&-RPM0TPmm4CA)fi+"FbZ)DQ3),cEQ&$BQ(MI)"CQr@fJ2Z&A& C(rGaLd%!Keh-l()%NLC@Z%9*DK`YEVd&Hq&f125Y'V0SJUQP9fe-[MMYMEVXJ$[ JQ+$8DCe&[!XZC)$@'X`U'p2BPCI'+e'8Q,m@-a-"qXe055@qb*)F02bLHpP'F!' )43!M2aNF5q0#08ijS*!!K*R-rM4UZ&$d)[5)Y&ZaX)dGI4m"VhR4RBIjL"RGQ(J K"fP-D-!6l%!V4ZF-8U[*LY9BRF"CpUkKK5(l2Q1qk%AhdRB2G'Bdr$#V)&-S3%E 6L'!TUS3[K*-%bS*X`5a(H!"H8R,%"5qkXahG'`d%dm3Qdf4*p0EiN!!L"1bJ-50 )%J!5-M8XApak+qEVk2Ybr-GIG'GfN!$,@(p,M3aQPiE84VIM1qdhDZibZ#5)#%h %`$!PJYi!CR[J"FIYk2[5rC`1pc500HQV19,f!i68h(+iehf2%hrJeSdEEp8mc8Q j1J!D!hhZ@`qdBf%[TbDk#"-pJ0k#0lB8j!SIF@F3cBk6pS,c&[UQZmI`Ak@H6h8 IJqi1'JP%0@i0QFL#il#hMMfCDMMU"AHBjK"$*4VCQK4EEhhi4+m($&L&TV5ii9V !-a[lHReJ[hXUTmB,#"AH-k61pIH2YGmG&VrVdFeimEIVVNIaqcT5Gpf&h,GqN!$ U9E,I2HhBVXdl0qqmDbGk)HE25m4hp$9l(Q"!-FUhSYXQC(EHY@RRShLa`YRCeq` jpl6GQhCZhX("%1rF[(RRcVX`Tal@q@'UfHLIZF0hBaVdB2(@R9[e-MBaYrR42FI q+YAYJcpcch"fEHB'd%J@Y8PhI*4&QarGIIJeq!"lGZrHK8m#*aZA2qZ'MQ&+cXE PEHA@QF)'C812lYjcl%dhppLH2E[[eV9lqJDBmBalPR-Aei+40c2#$VMqcCLD+0L %pHhD[IXEcQjeYl1V3$UJL[M!'[YQQElA(Ed,Xh!4HR0XJN9JJhUE+0EE&FaXG6C RXa-!XI2TMTG5SdapfJhZ-9[&#&X"0+5Fc@F#RaE1GfRJFPaZ6AIEZ3RIj1SIj)2 (hK)%XPUQjhih%CL%#E0B#KFU&36r9PPHrh+R21@1!d3&+8$)A9Z"8$3KXRGb(aJ 4#q%@(YRm#'%[B%1ArRe-IY)pEcGhb4%j,lFS51@Dp3D!%RER(M#Ze26[BZ)6-*9 fFfh!HhcRql!3S3i""Y#%U68S#'L*mH[I3rPZGd`(&k%44K`4P%)FA+ZQ)&4`iA` pZZI``G3!%ajh5p#I5j!!SC%5#Q9[B6'ZeC!!hUlG(BF2T*UIrjJl!S4N3+[R)bQ 3!&Se[Q8$Mqjf1PlS%bq2ZX9J39%"dPNc""2X[QRAVMGKXU@8`irFBD60Zm&,C&+ 3!-!MN!!0GqrHiacVF!mrQqTeaSmJIAVGR'0[2HfmjEaec$RQ2*'U$ccNUX-r2KR NIrh!5r(r0['aIc,1r)XX22EI,0Pr"LVr4iPYe3@*rqjd0YkM3pmmYqcFFmp'F&j SANepFh0S([jqe)S3rKC8U+Dq$RpV%RpVZ,ka#Im(-I[Fl(0P'[jhcPrqrbElVcV 2`#Cbq-G8elRVA(AlHPhebrrXkIh,TZ*A[MiIllZKQrRrCIj3I2LA6GBcN!!iLrq A!$H4+KIrl4cr%q!P2hVP+*b--UAFN`'N#TKb!#K(dS9-qejk#6HF,k()c@GH[B4 66'BcP!XSf"$,`9'XcdeX56`-hjGpArlIVSRqlh'rG%h5d8YmRq*AlZJSIQA,@8U 0"drbhbpEem5JdQp3Q@C8IlT"j`#$cJbM@M-05V--5V10kX+Ipa5djKUdjKR9N!" [8&YJ8)[*IiPDr2A6AJ!K3aALp2GSYMU+SeImFqA#l%`36+DA@@-cQ9jQMFeNHKP d4-l,H$80AUCrXM4Q!,K#-ljCKR$-!rMp,f6qerqJ$&4qT[L9V4q$63lPmk[rL0b (P2)BrL*d5d[,KTD0,Cp[ZD1PTE[PMCBA@hDLa&IKacF3X$Am-CA4*cY`i6DYa4X cAVcJ$pi(&mjc['#Ji4-GZI"Ah&b&6p$jFL8R[TTM8rK!P8hKHfdkjFH3!,ff#Rq c)0IBFEeS!&G$Tq+jq1L(6MQjX0p1c)J`&q1rDm$4If[eYClX`(rE0"C[X0"N)[J N'2jEKMU+%(DBK1+YdAYM"XBD,$8TKE&Q`LcIdFci'Pm$%`S*kp&jMK[*,ZQiHI2 ef@dR5I$EBR-VmFBIU+dNYrk6hiBT0ehim+Gr%2p"r)lZclqamF80m0[@*rbf-l1 e$MKjJGr@qHFAiAf"8PAjIcJrq'hpKfF&G0'F[IKV5AYTRq@M3@`[fM'!+BE2qrB Pa(6VP&CLaAA[08iE2Xf))BaY&pqEF0VflUA6pYXc0mcMZmehfdN1A,EBKbT`5h8 r#Vpj8JfA6Fl2'RKqaNbQPeNM'FpDbe4(BmCLd`9br0CJE$RdY4NPQ5ae&0q(l%r JIcXJFG)&[qe5[+F"[p1K8IklhhBp,0R2qHlSrN(h(GfIZl$e`ZX[A)pIaAMMYjf IIIj*$[bff,5KaE'Cq-Iq(iC5q`2l`@m6Bd`pL@p@daM$plZIP*biD)j1LEIf*,f eE2505LCCK-p,X"AIj[-5-SA0bpFRP*I'B&h[T2MYLfl*RI$JrYMhabFpm1!Ha2X jT8iP9NpQiF&Pqp("JBpf00X2GQ'Qm&mbq[-8eS0E!d21ZR'U)A(`GM+(ZlHCHF@ a5r+8'REa2rP`NhXfG2IprQ,$UGGRFh*lpqCldLG+*,NelArCHI4E&V!Hhj6$9q9 NHlERp9c&3,f2[ZGi(YqQIlG!pQ3rAkHU'qU@K9*0&M8dV0!C8"0l*A#a4[R5jXb DHQ+X[ZFXeld*2I*cKR&IqG1R9Fke1a@UFjI2K'a8(jTH1A(qY9GGr*0cFY5F,2q FqU2II[lPbdjl2VE`#cp05p[`KD'r124Lr[JjRkLiCZF0rX(hEKhhk+#9Q9GHHqH -9DGmm*2eDiIFrr',$ahT`DIcB*&1Q99jhk5&3%@QZNme)b*kdr$M`2NU6lBBJ%V p[KU*kV6!TDYUDkkCAlXb[%dqaVG)S5i,GGKED%QiTRjP3f1iUDQqNAAC!1'LaR# S,KaH%Pi5DUi20BA$SHEPY8fKKYUDjP@0B5k"U0lPJelp9e46,Cam!G8Vra(9F(" rM@TCc`G[r#%&reK"Y@r!cMr09k'mY*erFZll"NflBX-MTdqq2qq+d89VjKmGGZV ThhfLmNqHDrp4f8p(IkRUjA'$Lapkr*5"'`lZKdJrQ35@Zhci*`6rLQAFRCiN!-Y rp2pKHI8rBYPm`4)l5eY[p!abTNEV4j[r0HEpA-lLc80S+'62Z@6q&)d1G'kCe6- @QC'D#'S@jZIR,``X[(,R$cYfr2@1'bFe"%G8ql,['Aa,RMm[Er6#Zp4jRmYmk6E RXVBqm*rX!IX2q%jSb[q*IA&D6Vl!ijHI6)$(L`em,pMaq2fc[hcRbm9jQhr8fMA dm+'K8AI!U!ceq$d$(b`-A3@Il535U%9i$rbaEq#GRT2qiUAh$m-!DRKDSaSdCrV NqDT)EEZNFNS9rYV&`j-[QcY2CILHaKpEU&0TrP&6,r[ckD$&+qI80c3MAVSkh0L %relC@&[6"!lhhi`BR1Er-Z,h)YikII)Xa[I9eUaJr81)@Ir)T&Q9Z*!!pcmjmk` PXa$[VjU,HCArm2bCFbl"20&jdkqFS[aT"C@Ac!C+dmkYR$iIrG)qY+5QH6(L'Ek RJ"B,e,p43a'+S[3pVH$qk5X`hei&*p2hJZ6fU8QB6d5`laNe%lP25HiYmYAT)c@ P-lG*-`3qF+[8CYcRi,QHZ3EKAE@"ZH[f5qicc(hU@XRG`0aDQ8qe-RI$9C,Eb&` Vh%Pb2R-E[klP!R1h`GA&Fc0cAlaFFTpPlNX&NVZ&ZDrVllEGbY`@c@HIBfiV0C0 5RfIZEZS)"8bSN!$bDacX95#,E2ce$DRl5%J0N!!jJ*@2)5Ilmch0hKNMG$rQ#RA [IDkEVJ,#h,jR*#Hlm6dVZ5ek*ZPGTfGL,R0*BUDX+a-cC3Zf4SSV(Y!i`,rQ43i HSfECN!"U@*(qiTaVHI-88X2[4qlf(r`&H`3&icSh82CKFZ)`QYbq41i8Z))f0d4 M9c"p+YaMH9E#RG,I+CDkBH"-lNDY4@k3!0k0`YqY18f%LGBb`ap2Ih(@GlNRj*j &$RrD4h+[)[FJX)A[HJbh1,X&A%+hLEN[JM-%SlRcep4I8PXA*UIQeUbSE`S[@G6 B@,mQZliKA#HC3,+UUAj&lC)#Ve651C*UV&ff[$PE8N[Uep5P6i-N5+qUE@V1RF4 'i8B1@$"l69fi-34C8,qUX5DF6X*1*eX2Q,LiIP9Ip2m!N!36"J!!: \ No newline at end of file diff --git a/MacstodonConstants.py b/MacstodonConstants.py old mode 100644 new mode 100755 index 6acf93c..bd9c43e --- a/MacstodonConstants.py +++ b/MacstodonConstants.py @@ -1 +1 @@ -""" Macstodon - a Mastodon client for classic Mac OS MIT License Copyright (c) 2022-2023 Scott Small and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # ######### # Constants # ######### DEBUG = 0 INITIAL_TOOTS = 5 VERSION = "0.4.4" \ No newline at end of file +""" Macstodon - a Mastodon client for classic Mac OS MIT License Copyright (c) 2022-2023 Scott Small and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # ######### # Constants # ######### DEBUG = 0 VERSION = "1.0" \ No newline at end of file diff --git a/MacstodonHelpers.py b/MacstodonHelpers.py old mode 100644 new mode 100755 index 39d5957..aa0369a --- a/MacstodonHelpers.py +++ b/MacstodonHelpers.py @@ -1 +1 @@ -""" Macstodon - a Mastodon client for classic Mac OS MIT License Copyright (c) 2022-2023 Scott Small and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # ############## # Python Imports # ############## import EasyDialogs import Qd import string import time import urllib import W from Wlists import List # ########## # My Imports # ########## from MacstodonConstants import DEBUG, VERSION # ######### # Functions # ######### def cleanUpUnicode(content): """ Do the best we can to manually clean up unicode stuff """ content = string.replace(content, "\\u003e", ">") content = string.replace(content, "\\u003c", "<") content = string.replace(content, "\\u0026", "&") content = string.replace(content, "‚Ķ", "...") content = string.replace(content, "‚Äô", "'") content = string.replace(content, "‚ħ", ".") content = string.replace(content, "‚Äî", "-") content = string.replace(content, "‚Äú", '"') content = string.replace(content, "‚Äù", '"') content = string.replace(content, """, '"') content = string.replace(content, "√©", "é") content = string.replace(content, "√∂", "ö") content = string.replace(content, "'", "'") content = string.replace(content, "&", "&") content = string.replace(content, ">", ">") content = string.replace(content, "<", "<") return content def decodeJson(data): """ 'Decode' the JSON by taking the advantage of the fact that it is very similar to a Python dict. This is a terrible hack, and you should never do this anywhere because we're literally eval()ing untrusted data from the 'net. I'm only doing it because it's fast and there's not a lot of other options for parsing JSON data in Python 1.5. """ data = string.replace(data, "null", "None") data = string.replace(data, "false", "0") data = string.replace(data, "true", "1") data = eval(data) return data def dprint(text): """ Prints a string to stdout if and only if DEBUG is true """ if DEBUG: print text def okDialog(text, size=None): """ Draws a modal dialog box with the given text and an OK button to dismiss the dialog. """ if not size: size = (360, 120) window = W.ModalDialog(size, "Macstodon %s - Message" % VERSION) window.label = W.TextBox((10, 10, -10, -40), text) window.ok_btn = W.Button((-80, -30, -10, -10), "OK", window.close) window.setdefaultbutton(window.ok_btn) window.open() def okCancelDialog(text, size=None): """ Draws a modal dialog box with the given text and OK/Cancel buttons. The OK button will close the dialog. The Cancel button will raise an Exception, which the caller is expected to catch. """ if not size: size = (360, 120) global dialogWindow dialogWindow = W.ModalDialog(size, "Macstodon %s - Message" % VERSION) def dialogExceptionCallback(): dialogWindow.close() raise KeyboardInterrupt dialogWindow.label = W.TextBox((10, 10, -10, -40), text) dialogWindow.cancel_btn = W.Button((-160, -30, -90, -10), "Cancel", dialogExceptionCallback) dialogWindow.ok_btn = W.Button((-80, -30, -10, -10), "OK", dialogWindow.close) dialogWindow.setdefaultbutton(dialogWindow.ok_btn) dialogWindow.open() def handleRequest(app, path, data = None, use_token = 0): """ HTTP request wrapper """ try: pb = EasyDialogs.ProgressBar(maxval=3) if data == {}: data = "" elif data: data = urllib.urlencode(data) prefs = app.getprefs() url = "%s%s" % (prefs.server, path) dprint(url) dprint(data) dprint("connecting") pb.label("Connecting...") pb.inc() try: if use_token: urlopener = TokenURLopener(prefs.token) handle = urlopener.open(url, data) else: handle = urllib.urlopen(url, data) except IOError: del pb errmsg = "Unable to open a connection to: %s.\rPlease check that your SSL proxy is working properly and that the URL starts with 'http'." okDialog(errmsg % url) return None except TypeError: del pb errmsg = "The provided URL is malformed: %s.\rPlease check that you have typed the URL correctly." okDialog(errmsg % url) return None dprint("reading http headers") dprint(handle.info()) dprint("reading http body") pb.label("Fetching data...") pb.inc() try: data = handle.read() except IOError: del pb errmsg = "The connection was closed by the remote server while Macstodon was reading data.\rPlease check that your SSL proxy is working properly." okDialog(errmsg) return None try: handle.close() except IOError: pass pb.label("Parsing data...") pb.inc() dprint("parsing response json") try: decoded = decodeJson(data) dprint(decoded) pb.label("Done.") pb.inc() time.sleep(0.5) del pb return decoded except: del pb dprint("ACK! JSON Parsing failure :(") dprint("This is what came back from the server:") dprint(data) okDialog("Error parsing JSON response from the server.") return None except KeyboardInterrupt: # the user pressed cancel in the progress bar window return None # ####### # Classes # ####### class ImageWidget(W.Widget): """ A widget that displays an image. The image should be passed in as a PixMapWrapper. """ def __init__(self, possize, pixmap=None): W.Widget.__init__(self, possize) # Set initial image self._imgloaded = 0 self._pixmap = None if pixmap: self.setImage(pixmap) def close(self): """ Destroys the widget and frees up its memory """ W.Widget.close(self) del self._imgloaded del self._pixmap def setImage(self, pixmap): """ Loads a new image into the widget. The image will be automatically scaled to the size of the widget. """ self._pixmap = pixmap self._imgloaded = 1 if self._parentwindow: self.draw() def clearImage(self): """ Unloads the image from the widget without destroying the widget. Use this to make the widget draw an empty square. """ self._imgloaded = 0 Qd.EraseRect(self._bounds) if self._parentwindow: self.draw() self._pixmap = None def draw(self, visRgn = None): """ Draw the image within the widget if it is loaded """ if self._visible: if self._imgloaded: self._pixmap.blit( x1=self._bounds[0], y1=self._bounds[1], x2=self._bounds[2], y2=self._bounds[3], port=self._parentwindow.wid.GetWindowPort() ) class TitledEditText(W.Group): """ A text edit field with a title and optional scrollbars attached to it. Shamelessly stolen from MacPython's PyEdit. Modified to also allow setting the title, and add scrollbars. """ def __init__(self, possize, title, text="", readonly=0, vscroll=0, hscroll=0): W.Group.__init__(self, possize) self.title = W.TextBox((0, 0, 0, 16), title) if vscroll and hscroll: editor = W.EditText((0, 16, -15, -15), text, readonly=readonly) self._barx = W.Scrollbar((0, -16, -15, 16), editor.hscroll, max=32767) self._bary = W.Scrollbar((-16, 16,0, -15), editor.vscroll, max=32767) elif vscroll: editor = W.EditText((0, 16, -15, 0), text, readonly=readonly) self._bary = W.Scrollbar((-16, 16, 0, 0), editor.vscroll, max=32767) elif hscroll: editor = W.EditText((0, 16, 0, -15), text, readonly=readonly) self._barx = W.Scrollbar((0, -16, 0, 16), editor.hscroll, max=32767) else: editor = W.EditText((0, 16, 0, 0), text, readonly=readonly) self.edit = editor def setTitle(self, value): self.title.set(value) def set(self, value): self.edit.set(value) def get(self): return self.edit.get() class TokenURLopener(urllib.FancyURLopener): """ Extends urllib.FancyURLopener to add the Authorization header with a bearer token. """ def __init__(self, token, *args): apply(urllib.FancyURLopener.__init__, (self,) + args) self.addheaders.append(("Authorization", "Bearer %s" % token)) class TwoLineListWithFlags(List): """ Modification of MacPython's TwoLineList to support flags. """ LDEF_ID = 468 def createlist(self): import List self._calcbounds() self.SetPort() rect = self._bounds rect = rect[0]+1, rect[1]+1, rect[2]-16, rect[3]-1 self._list = List.LNew(rect, (0, 0, 1, 0), (0, 28), self.LDEF_ID, self._parentwindow.wid, 0, 1, 0, 1) self._list.selFlags = self._flags self.set(self.items) class TimelineList(W.Group): """ A TwoLineListWithFlags that also has a title attached to it. Based on TitledEditText. """ def __init__(self, possize, title, items = None, btnCallback = None, callback = None, flags = 0, cols = 1, typingcasesens=0): W.Group.__init__(self, possize) self.title = W.TextBox((0, 2, 0, 16), title) self.btn = W.Button((-50, 0, 0, 16), "Refresh", btnCallback) self.list = TwoLineListWithFlags((0, 24, 0, 0), items, callback, flags, cols, typingcasesens) def setTitle(self, value): self.title.set(value) def set(self, items): self.list.set(items) def get(self): return self.list.items def getselection(self): return self.list.getselection() def setselection(self, selection): return self.list.setselection(selection) \ No newline at end of file +""" Macstodon - a Mastodon client for classic Mac OS MIT License Copyright (c) 2022-2023 Scott Small and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # ############## # Python Imports # ############## import EasyDialogs import Fm import formatter import htmllib import ic import Lists import macfs import os import Qd import QuickDraw import string import TE import time import urllib import urlparse import W from TextEdit import * from Wlists import List # ########## # My Imports # ########## from MacstodonConstants import DEBUG, VERSION # ################### # Third-Party Imports # ################### from third_party.PixMapWrapper import PixMapWrapper # ######### # Functions # ######### def getFilenameFromURL(url): """ Returns the file name, including extension, from a URL. i.e. http://www.google.ca/foo/bar/baz/asdf.jpg returns "asdf.jpg" """ parsed_url = urlparse.urlparse(url) path = parsed_url[2] file_name = os.path.basename(string.replace(path, "/", ":")) return file_name def cleanUpUnicode(content): """ Do the best we can to manually clean up unicode stuff """ # The text is UTF-8, but is being interpreted as MacRoman. # First step is to convert the extended characters into # MacRoman so that they display correctly. content = string.replace(content, "√Ñ", "Ä") content = string.replace(content, "√Ö", "Å") content = string.replace(content, "√á", "Ç") content = string.replace(content, "√â", "É") content = string.replace(content, "√ë", "Ñ") content = string.replace(content, "√ñ", "Ö") content = string.replace(content, "√ú", "Ü") content = string.replace(content, "√°", "á") content = string.replace(content, "√†", "à") content = string.replace(content, "√¢", "â") content = string.replace(content, "√§", "ä") content = string.replace(content, "√£", "ã") content = string.replace(content, "√•", "å") content = string.replace(content, "√ß", "ç") content = string.replace(content, "√©", "é") content = string.replace(content, "√®", "è") content = string.replace(content, "√™", "ê") content = string.replace(content, "√´", "ë") content = string.replace(content, "√≠", "í") content = string.replace(content, "√¨", "ì") content = string.replace(content, "√Æ", "î") content = string.replace(content, "√Ø", "ï") content = string.replace(content, "√±", "ñ") content = string.replace(content, "√≥", "ó") content = string.replace(content, "√≤", "ò") content = string.replace(content, "√¥", "ô") content = string.replace(content, "√∂", "ö") content = string.replace(content, "√µ", "õ") content = string.replace(content, "√∫", "ú") content = string.replace(content, "√π", "ù") content = string.replace(content, "√ª", "û") content = string.replace(content, "√º", "ü") content = string.replace(content, "‚Ć", "†") content = string.replace(content, "¬∞", "°") content = string.replace(content, "¬¢", "¢") content = string.replace(content, "¬£", "£") content = string.replace(content, "¬ß", "§") content = string.replace(content, "‚Ä¢", "•") content = string.replace(content, "¬∂", "¶") content = string.replace(content, "√ü", "ß") content = string.replace(content, "¬Æ", "®") content = string.replace(content, "¬©", "©") content = string.replace(content, "‚Ñ¢", "™") content = string.replace(content, "¬¥", "´") content = string.replace(content, "¬®", "¨") content = string.replace(content, "‚â†", "≠") content = string.replace(content, "√Ü", "Æ") content = string.replace(content, "√ò", "Ø") content = string.replace(content, "‚àû", "∞") content = string.replace(content, "¬±", "±") content = string.replace(content, "‚â§", "≤") content = string.replace(content, "‚â•", "≥") content = string.replace(content, "¬•", "¥") content = string.replace(content, "¬µ", "µ") content = string.replace(content, "‚àÇ", "∂") content = string.replace(content, "‚àë", "∑") content = string.replace(content, "‚àè", "∏") content = string.replace(content, "œÄ", "π") content = string.replace(content, "‚à´", "∫") content = string.replace(content, "¬™", "ª") content = string.replace(content, "¬∫", "º") content = string.replace(content, "Œ©", "Ω") content = string.replace(content, "√¶", "æ") content = string.replace(content, "√∏", "ø") content = string.replace(content, "¬ø", "¿") content = string.replace(content, "¬°", "¡") content = string.replace(content, "¬¨", "¬") content = string.replace(content, "‚àö", "√") content = string.replace(content, "∆í", "ƒ") content = string.replace(content, "‚âà", "≈") content = string.replace(content, "‚àÜ", "∆") content = string.replace(content, "¬´", "«") content = string.replace(content, "¬ª", "»") content = string.replace(content, "‚Ķ", "…") content = string.replace(content, "√Ä", "À") content = string.replace(content, "√É", "Ã") content = string.replace(content, "√ï", "Õ") content = string.replace(content, "≈í", "Œ") content = string.replace(content, "≈ì", "œ") content = string.replace(content, "‚Äì", "–") content = string.replace(content, "‚Äî", "—") content = string.replace(content, "‚Äú", "“") content = string.replace(content, "‚Äù", "”") content = string.replace(content, "‚Äò", "‘") content = string.replace(content, "‚Äô", "’") content = string.replace(content, "√∑", "÷") content = string.replace(content, "‚óä", "◊") content = string.replace(content, "√ø", "ÿ") content = string.replace(content, "≈∏", "Ÿ") content = string.replace(content, "‚ÅÑ", "⁄") content = string.replace(content, "‚Ǩ", "€") content = string.replace(content, "‚Äπ", "‹") content = string.replace(content, "‚Ä∫", "›") content = string.replace(content, "Ô¨Å", "fi") content = string.replace(content, "Ô¨Ç", "fl") content = string.replace(content, "‚Ä°", "‡") content = string.replace(content, "¬∑", "·") content = string.replace(content, "‚Äö", "‚") content = string.replace(content, "‚Äû", "„") content = string.replace(content, "‚Ä∞", "‰") content = string.replace(content, "√Ç", "Â") content = string.replace(content, "√ä", "Ê") content = string.replace(content, "√Å", "Á") content = string.replace(content, "√ã", "Ë") content = string.replace(content, "√à", "È") content = string.replace(content, "√ç", "Í") content = string.replace(content, "√é", "Î") content = string.replace(content, "√è", "Ï") content = string.replace(content, "√å", "Ì") content = string.replace(content, "√ì", "Ó") content = string.replace(content, "√î", "Ô") content = string.replace(content, "Ô£ø", "") content = string.replace(content, "√í", "Ò") content = string.replace(content, "√ö", "Ú") content = string.replace(content, "√õ", "Û") content = string.replace(content, "√ô", "Ù") content = string.replace(content, "ƒ±", "ı") content = string.replace(content, "ÀÜ", "ˆ") content = string.replace(content, "Àú", "˜") content = string.replace(content, "¬Ø", "¯") content = string.replace(content, "Àò", "˘") content = string.replace(content, "Àô", "˙") content = string.replace(content, "Àö", "˚") content = string.replace(content, "¬∏", "¸") content = string.replace(content, "Àù", "˝") content = string.replace(content, "Àõ", "˛") content = string.replace(content, "Àá", "ˇ") # Next, convert unicode codepoints back into characters content = string.replace(content, "\\u003e", ">") content = string.replace(content, "\\u003c", "<") content = string.replace(content, "\\u0026", "&") # Lastly, convert HTML entities back into characters content = string.replace(content, """, '"') content = string.replace(content, "'", "'") content = string.replace(content, "&", "&") content = string.replace(content, ">", ">") content = string.replace(content, "<", "<") # After conversion, any characters above byte 127 are # outside the MacRoman character set and should be # stripped. for char in content: if ord(char) > 127: content = string.replace(content, char, "") return content def decodeJson(data): """ 'Decode' the JSON by taking the advantage of the fact that it is very similar to a Python dict. This is a terrible hack, and you should never do this anywhere because we're literally eval()ing untrusted data from the 'net. I'm only doing it because it's fast and there's not a lot of other options for parsing JSON data in Python 1.5. """ data = string.replace(data, '":null', '":None') data = string.replace(data, '":false', '":0') data = string.replace(data, '":true', '":1') data = eval(data) return data def dprint(text): """ Prints a string to stdout if and only if DEBUG is true """ if DEBUG: print text def okDialog(text, size=None): """ Draws a modal dialog box with the given text and an OK button to dismiss the dialog. """ if not size: size = (360, 120) window = W.ModalDialog(size, "Macstodon %s - Message" % VERSION) window.label = W.TextBox((10, 10, -10, -40), text) window.ok_btn = W.Button((-80, -30, -10, -10), "OK", window.close) window.setdefaultbutton(window.ok_btn) window.open() def okCancelDialog(text, size=None): """ Draws a modal dialog box with the given text and OK/Cancel buttons. The OK button will close the dialog. The Cancel button will raise an Exception, which the caller is expected to catch. """ if not size: size = (360, 120) global dialogWindow dialogWindow = W.ModalDialog(size, "Macstodon %s - Message" % VERSION) def dialogExceptionCallback(): dialogWindow.close() raise KeyboardInterrupt dialogWindow.label = W.TextBox((10, 10, -10, -40), text) dialogWindow.cancel_btn = W.Button((-160, -30, -90, -10), "Cancel", dialogExceptionCallback) dialogWindow.ok_btn = W.Button((-80, -30, -10, -10), "OK", dialogWindow.close) dialogWindow.setdefaultbutton(dialogWindow.ok_btn) dialogWindow.open() def attachmentsDialog(media_attachments): """ Draws a modal dialog box with Download and Close buttons. Between the text and buttons a list is drawn containing attachments. Clicking on an attachment, then clicking on the Download button will save the contents of the attachment to disk. Clicking the Close button will close the window. """ if len(media_attachments) == 0: okDialog("The selected toot contains no attachments.") return global attachmentsWindow, attachmentsData attachmentsFormatted = [] attachmentsData = [] for attachment in media_attachments: if attachment["description"] is not None: desc = cleanUpUnicode(attachment["description"]) listStr = desc + "\r" else: desc = None listStr = "No description\r" if attachment["type"] == "image": listStr = listStr + "Image, %s" % attachment["meta"]["original"]["size"] elif attachment["type"] == "video": listStr = listStr + "Video, %s, %s" % (attachment["meta"]["size"], attachment["meta"]["length"]) elif attachment["type"] == "gifv": listStr = listStr + "GIFV, %s, %s" % (attachment["meta"]["size"], attachment["meta"]["length"]) elif attachment["type"] == "audio": listStr = listStr + "Audio, %s" % attachment["meta"]["length"] else: listStr = listStr + "Unknown" attachmentsFormatted.append(listStr) attachmentsData.append({"url": attachment["url"], "alt": desc}) attachmentsWindow = W.ModalDialog((360, 240), "Macstodon %s - Attachments" % VERSION) def openAttachmentCallback(): """ Run when the user clicks the Download button """ selected = attachmentsWindow.attachments.getselection() if len(selected) > 0: url = attachmentsData[selected[0]]["url"] default_file_name = getFilenameFromURL(url) fss, ok = macfs.StandardPutFile('Save as:', default_file_name) if not ok: return 1 file_path = fss.as_pathname() file_name = os.path.split(file_path)[-1] urllib.urlretrieve(url, file_path) okDialog("Successfully downloaded '%s'!" % file_name) else: okDialog("Please select an attachment first.") def altTextCallback(): """ Run when the user clicks the Alt Text button """ selected = attachmentsWindow.attachments.getselection() if len(selected) > 0: alt_text = attachmentsData[selected[0]]["alt"] if alt_text is not None: okDialog(alt_text) else: okDialog("This attachment doesn't have any alt text.") else: okDialog("Please select an attachment first.") text = "The following attachments were found in the selected toot. " \ "Click on an attachment in the list, then click on the Download button " \ "to download it to your computer. You can also click on the Alt Text " \ "button to view the attachment's alt text in a dialog." attachmentsWindow.label = W.TextBox((10, 10, -10, -60), text) attachmentsWindow.attachments = TwoLineListWithFlags((10, 76, -10, -42), attachmentsFormatted, callback=None, flags = Lists.lOnlyOne, cols = 1, typingcasesens=0) attachmentsWindow.alt_btn = W.Button((-240, -30, -170, -10), "Alt Text", altTextCallback) attachmentsWindow.close_btn = W.Button((-160, -30, -90, -10), "Close", attachmentsWindow.close) attachmentsWindow.download_btn = W.Button((-80, -30, -10, -10), "Download", openAttachmentCallback) attachmentsWindow.setdefaultbutton(attachmentsWindow.close_btn) attachmentsWindow.open() def linksDialog(le): """ Draws a modal dialog box with Open and Close buttons. Between the text and buttons a list is drawn containing links. Clicking on a link, then clicking on the Open button will open the link using the user's web browser. Clicking the Close button will close the window. """ global linksWindow, linksFormatted linksFormatted = [] for desc, url in le.anchors.items(): linksFormatted.append(desc + "\r" + url[0]) if len(linksFormatted) == 0: okDialog("The selected toot contains no links.") return linksWindow = W.ModalDialog((240, 240), "Macstodon %s - Links" % VERSION) def openLinkCallback(): """ Run when the user clicks the Open button """ selected = linksWindow.links.getselection() if len(selected) > 0: linkString = linksFormatted[selected[0]] linkParts = string.split(linkString, "\r") ic.launchurl(linkParts[1]) else: okDialog("Please select a link first.") text = "The following links were found in the selected toot. " \ "Click on a link in the list, then click on the Open button " \ "to open it with your browser." linksWindow.label = W.TextBox((10, 10, -10, -40), text) linksWindow.links = TwoLineListWithFlags((10, 56, -10, -42), linksFormatted, callback=None, flags = Lists.lOnlyOne, cols = 1, typingcasesens=0) linksWindow.close_btn = W.Button((-160, -30, -90, -10), "Close", linksWindow.close) linksWindow.open_btn = W.Button((-80, -30, -10, -10), "Open", openLinkCallback) linksWindow.setdefaultbutton(linksWindow.close_btn) linksWindow.open() def handleRequest(app, path, data = None, use_token = 0): """ HTTP request wrapper """ try: W.SetCursor("watch") pb = EasyDialogs.ProgressBar(maxval=3) if data == {}: data = "" elif data: data = urllib.urlencode(data) prefs = app.getprefs() url = "%s%s" % (prefs.server, path) dprint(url) dprint(data) dprint("connecting") pb.label("Connecting...") pb.inc() try: if use_token: urlopener = TokenURLopener(prefs.token) handle = urlopener.open(url, data) else: handle = urllib.urlopen(url, data) except IOError: del pb W.SetCursor("arrow") errmsg = "Unable to open a connection to: %s.\rPlease check that your SSL proxy is working properly and that the URL starts with 'http'." okDialog(errmsg % url) return None except TypeError: del pb W.SetCursor("arrow") errmsg = "The provided URL is malformed: %s.\rPlease check that you have typed the URL correctly." okDialog(errmsg % url) return None dprint("reading http headers") dprint(handle.info()) dprint("reading http body") pb.label("Fetching data...") pb.inc() try: data = handle.read() except IOError: del pb W.SetCursor("arrow") errmsg = "The connection was closed by the remote server while Macstodon was reading data.\rPlease check that your SSL proxy is working properly." okDialog(errmsg) return None try: handle.close() except IOError: pass pb.label("Parsing data...") pb.inc() dprint("parsing response json") try: decoded = decodeJson(data) dprint(decoded) pb.label("Done.") pb.inc() time.sleep(0.5) del pb W.SetCursor("arrow") return decoded except: del pb W.SetCursor("arrow") dprint("ACK! JSON Parsing failure :(") dprint("This is what came back from the server:") dprint(data) okDialog("Error parsing JSON response from the server.") return None except KeyboardInterrupt: # the user pressed cancel in the progress bar window W.SetCursor("arrow") return None # ####### # Classes # ####### class ImageWidget(W.ClickableWidget): """ A widget that displays an image. The image should be passed in as a PixMapWrapper. """ def __init__(self, possize, pixmap=None, callback=None): W.ClickableWidget.__init__(self, possize) self._callback = callback self._enabled = 1 # Set initial image self._imgloaded = 0 self._pixmap = None if pixmap: self.setImage(pixmap) def click(self, point, modifiers): """ Runs the callback if the user clicks on the image """ if not self._enabled: return if self._callback: return W.CallbackCall(self._callback, 0) def close(self): """ Destroys the widget and frees up its memory """ W.Widget.close(self) del self._imgloaded del self._pixmap def setImage(self, pixmap): """ Loads a new image into the widget. The image will be automatically scaled to the size of the widget. """ self._pixmap = pixmap self._imgloaded = 1 if self._parentwindow: self.draw() def clearImage(self): """ Unloads the image from the widget without destroying the widget. Use this to make the widget draw an empty square. """ self._imgloaded = 0 Qd.EraseRect(self._bounds) if self._parentwindow: self.draw() self._pixmap = None def draw(self, visRgn = None): """ Draw the image within the widget if it is loaded """ if self._visible: if self._imgloaded: if isinstance(self._pixmap, PixMapWrapper): self._pixmap.blit( x1=self._bounds[0], y1=self._bounds[1], x2=self._bounds[2], y2=self._bounds[3], port=self._parentwindow.wid.GetWindowPort() ) else: Qd.SetPort(self._parentwindow.wid.GetWindowPort()) Qd.DrawPicture(self._pixmap, self._bounds) class LinkExtractor(htmllib.HTMLParser): """ A very basic link extractor that gets the URL and associated text. Sourced from: https://oreilly.com/library/view/python-standard-library/0596000960/ch05s05.html """ def __init__(self, verbose=0): self.anchors = {} f = formatter.NullFormatter() htmllib.HTMLParser.__init__(self, f, verbose) def anchor_bgn(self, href, name, type): self.save_bgn() self.anchor = href def anchor_end(self): text = string.strip(self.save_end()) if self.anchor and text: self.anchors[text] = self.anchors.get(text, []) + [self.anchor] class ProfileBanner(ImageWidget): """ The ProfileBanner is just an ImageWidget that renders text atop it at a fixed location, using a specific font and style. """ def __init__(self, possize, drop_shadow, pixmap=None, display_name="", acct_name=""): ImageWidget.__init__(self, possize, pixmap) self.display_name = display_name self.acct_name = acct_name self.drop_shadow = drop_shadow def draw(self, visRgn = None): """ Draws the profile banner text using pure QuickDraw (instead of widgets) """ Qd.SetPort(self._parentwindow.wid.GetWindowPort()) ImageWidget.draw(self, visRgn) Qd.TextFont(Fm.GetFNum("Geneva")) # Display Name Qd.TextSize(14) Qd.TextFace(QuickDraw.bold) Qd.RGBForeColor((0,0,0)) if self.drop_shadow: rect = (self._bounds[0] + 7, self._bounds[1] + 70, self._bounds[2], 0) TE.TETextBox(self.display_name, rect, teJustLeft) Qd.RGBForeColor((65535,65535,65535)) rect = (self._bounds[0] + 5, self._bounds[1] + 68, self._bounds[2], 0) TE.TETextBox(self.display_name, rect, teJustLeft) # Account Name Qd.TextSize(9) Qd.TextFace(QuickDraw.normal) Qd.RGBForeColor((0,0,0)) if self.drop_shadow: rect = (self._bounds[0] + 7, self._bounds[1] + 90, self._bounds[2], 0) TE.TETextBox(self.acct_name, rect, teJustLeft) Qd.RGBForeColor((65535,65535,65535)) rect = (self._bounds[0] + 5, self._bounds[1] + 88, self._bounds[2], 0) TE.TETextBox(self.acct_name, rect, teJustLeft) Qd.RGBForeColor((0,0,0)) Qd.TextFont(Fm.GetFNum("Monaco")) def populate(self, display_name=None, acct_name=None): """ Sets the display and account names. """ if display_name: self.display_name = display_name if acct_name: self.acct_name = acct_name class ProfilePanel(W.Group): """ The ProfilePanel is my poor man's implementation of tabs. It uses three buttons to swap out the widget beneath them. """ def __init__(self, possize): W.Group.__init__(self, possize) self.title = W.TextBox((0, 2, 0, 16), "Bio") self.btnStats = W.Button((-40, 0, 40, 16), "Stats", self.statsCallback) self.btnLinks = W.Button((-90, 0, 40, 16), "Links", self.linksCallback) self.btnBio = W.Button((-140, 0, 40, 16), "Bio", self.bioCallback) # Bio editor = W.EditText((0, 24, -15, 0), "", readonly=1) self._bary = W.Scrollbar((-16, 24, 0, 0), editor.vscroll, max=32767) self.bioText = editor # Stats & Links self.list = TwoLineListWithFlags((0, 24, 0, 0), [], callback=None, flags = Lists.lOnlyOne, cols = 1, typingcasesens=0) self.linksData = [] self.statsData = [] self.toots = 0 self.following = 0 self.followers = 0 self.locked = "No" self.bot = "No" self.group = "No" self.discoverable = "No" self.noindex = "No" self.moved = "No" self.suspended = "No" self.limited = "No" # hide stats/links by default self.list.show(0) self.bioText.select(0) def statsCallback(self): """ Shows the stats pane and hides bio/links """ self.title.set("Stats") self.bioText.show(0) self.bioText.enable(0) self.bioText.select(0) self.bioText._selectable = 0 self._bary.show(0) self.list.show(1) self.list.enable(1) self.list.select(1) self.btnStats.draw() self.btnLinks.draw() self.btnBio.draw() self.list.set(self.statsData) def linksCallback(self): """ Shows the links pane and hides bio/stats """ self.title.set("Links") self.bioText.show(0) self.bioText.enable(0) self.bioText.select(0) self.bioText._selectable = 0 self._bary.show(0) self.list.show(1) self.list.enable(1) self.list.select(1) self.btnStats.draw() self.btnLinks.draw() self.btnBio.draw() self.list.set(self.linksData) def bioCallback(self): """ Shows the bio pane and hides stats/links """ self.title.set("Bio") self.list.show(0) self.list.enable(0) self.list.select(0) self.bioText.show(1) self.bioText.enable(1) self.bioText._selectable = 1 self._bary.show(1) self._bary.draw() self.btnStats.draw() self.btnLinks.draw() self.btnBio.draw() def setBio(self, value): """ Updates the content of the bio text """ self.bioText.set(value) def collectStatsData(self): """ Updates the content of the stats list """ self.statsData = [ "Toots\r%s" % self.toots, "Following\r%s" % self.following, "Followers\r%s" % self.followers, "Locked\r%s" % self.locked, "Bot\r%s" % self.bot, "Group\r%s" % self.group, "Discoverable\r%s" % self.discoverable, "NoIndex\r%s" % self.noindex, "Moved\r%s" % self.moved, "Suspended\r%s" % self.suspended, "Limited\r%s" % self.limited ] self.list.set(self.statsData) def setLinks(self, linksData): """ Updates the content of the links list """ self.linksData = linksData self.list.set(self.linksData) def setToots(self, num): """ Updates the user's toot count """ self.toots = num self.collectStatsData() def setFollowers(self, num): """ Updates the user's followers count """ self.followers = num self.collectStatsData() def setFollowing(self, num): """ Updates the user's following count """ self.following = num self.collectStatsData() def setLocked(self, flag): if flag: self.locked = "Yes" else: self.locked = "No" self.collectStatsData() def setBot(self, flag): if flag: self.bot = "Yes" else: self.bot = "No" self.collectStatsData() def setDiscoverable(self, flag): if flag: self.discoverable = "Yes" else: self.discoverable = "No" self.collectStatsData() def setNoIndex(self, flag): if flag: self.noindex = "Yes" else: self.noindex = "No" self.collectStatsData() def setMoved(self, flag): if flag: self.moved = "Yes" else: self.moved = "No" self.collectStatsData() def setSuspended(self, flag): if flag: self.suspended = "Yes" else: self.suspended = "No" self.collectStatsData() def setLimited(self, flag): if flag: self.limited = "Yes" else: self.limited = "No" self.collectStatsData() class TitledEditText(W.Group): """ A text edit field with a title and optional scrollbars attached to it. Shamelessly stolen from MacPython's PyEdit. Modified to also allow setting the title, and add scrollbars. """ def __init__(self, possize, title, text="", readonly=0, vscroll=0, hscroll=0): W.Group.__init__(self, possize) self.title = W.TextBox((0, 0, 0, 16), title) if vscroll and hscroll: editor = W.EditText((0, 16, -15, -15), text, readonly=readonly) self._barx = W.Scrollbar((0, -16, -15, 16), editor.hscroll, max=32767) self._bary = W.Scrollbar((-16, 16,0, -15), editor.vscroll, max=32767) elif vscroll: editor = W.EditText((0, 16, -15, 0), text, readonly=readonly) self._bary = W.Scrollbar((-16, 16, 0, 0), editor.vscroll, max=32767) elif hscroll: editor = W.EditText((0, 16, 0, -15), text, readonly=readonly) self._barx = W.Scrollbar((0, -16, 0, 16), editor.hscroll, max=32767) else: editor = W.EditText((0, 16, 0, 0), text, readonly=readonly) self.edit = editor def setTitle(self, value): self.title.set(value) def set(self, value): self.edit.set(value) def get(self): return self.edit.get() class TokenURLopener(urllib.FancyURLopener): """ Extends urllib.FancyURLopener to add the Authorization header with a bearer token. """ def __init__(self, token, *args): apply(urllib.FancyURLopener.__init__, (self,) + args) self.addheaders.append(("Authorization", "Bearer %s" % token)) class TwoLineListWithFlags(List): """ Modification of MacPython's TwoLineList to support flags. """ LDEF_ID = 468 def createlist(self): import List self._calcbounds() self.SetPort() rect = self._bounds rect = rect[0]+1, rect[1]+1, rect[2]-16, rect[3]-1 self._list = List.LNew(rect, (0, 0, 1, 0), (0, 28), self.LDEF_ID, self._parentwindow.wid, 0, 1, 0, 1) self._list.selFlags = self._flags self.set(self.items) class TimelineList(W.Group): """ A TwoLineListWithFlags that also has a title attached to it. Based on TitledEditText. """ def __init__(self, possize, title, items = None, btnCallback = None, callback = None, flags = 0, cols = 1, typingcasesens=0): W.Group.__init__(self, possize) self.title = W.TextBox((0, 2, 0, 16), title) self.btn = W.Button((-50, 0, 0, 16), "Refresh", btnCallback) self.list = TwoLineListWithFlags((0, 24, 0, 0), items, callback, flags, cols, typingcasesens) def setTitle(self, value): self.title.set(value) def set(self, items): self.list.set(items) def get(self): return self.list.items def getselection(self): return self.list.getselection() def setselection(self, selection): return self.list.setselection(selection) \ No newline at end of file diff --git a/MacstodonSplash.py b/MacstodonSplash.py old mode 100644 new mode 100755 index 3b6352e..4323c87 --- a/MacstodonSplash.py +++ b/MacstodonSplash.py @@ -1 +1 @@ -""" Macstodon - a Mastodon client for classic Mac OS MIT License Copyright (c) 2022-2023 Scott Small and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ """ A quick note from Scott: This file has been almost copied/pasted verbatim from the MacPython IDE, which is why it looks nothing like the other source files style-wise. Changes I made here include: - updated the resources to use the Macstodon logo and give the dialog a black BG - Bugfix to the TextFont calls which were failing and causing the progress bar to not render (the MacPython IDE is affected by this as well, lol) - Fixed the _real__import__ calls which were only passing the name and not the other params, causing crashes when trying to run this in the IDE - Changed the text color to white and changed the text itself - Removed the easter egg """ import Dlg import Res from MacstodonConstants import VERSION splash = Dlg.GetNewDialog(468, -1) splash.DrawDialog() import Qd, TE, Fm, sys _real__import__ = None def install_importhook(): global _real__import__ import __builtin__ if _real__import__ is None: _real__import__ = __builtin__.__import__ __builtin__.__import__ = my__import__ def uninstall_importhook(): global _real__import__ if _real__import__ is not None: import __builtin__ __builtin__.__import__ = _real__import__ _real__import__ = None _progress = 0 def importing(module): global _progress Qd.SetPort(splash) Qd.TextFont(Fm.GetFNum("Geneva")) Qd.TextSize(9) rect = (85, 270, 415, 286) Qd.RGBForeColor((65535,65535,65535)) if module: TE.TETextBox('Importing: ' + module, rect, 0) if not _progress: Qd.FrameRect((85, 286, 415, 294)) pos = min(86 + 330 * _progress / 77, 414) Qd.PaintRect((86, 287, pos, 293)) _progress = _progress + 1 else: Qd.EraseRect(rect) Qd.PaintRect((86, 287, pos, 293)) Qd.RGBForeColor((0,0,0)) def my__import__(name, globals=None, locals=None, fromlist=None): try: return sys.modules[name] except KeyError: try: importing(name) except: try: rv = _real__import__(name, globals, locals, fromlist) finally: uninstall_importhook() return rv return _real__import__(name, globals, locals, fromlist) install_importhook() kHighLevelEvent = 23 import Win from Fonts import * from QuickDraw import * from TextEdit import * import string _keepsplashscreenopen = 0 abouttext1 = """Macstodon %s A basic Mastodon client for Classic Mac OS by Scott Small, @smallsco@oldbytes.space GitHub: https://github.com/smallsco/macstodon Icon and logo design by MhzModels Built with MacPython %s %s Written by Guido van Rossum with Jack Jansen (and others)""" def nl2return(text): return string.join(string.split(text, '\n'), '\r') def UpdateSplash(drawdialog = 0): if drawdialog: splash.DrawDialog() drawtext() Win.ValidRect(splash.GetWindowPort().portRect) def drawtext(): Qd.SetPort(splash) Qd.TextFont(Fm.GetFNum("Geneva")) Qd.TextSize(9) Qd.RGBForeColor((65535,65535,65535)) rect = (10, 135, 487, 270) import __main__ abouttxt = nl2return(abouttext1 % (VERSION, sys.version, sys.copyright)) TE.TETextBox(abouttxt, rect, teJustCenter) Qd.RGBForeColor((0,0,0)) UpdateSplash(1) def wait(): import Evt from Events import * global splash try: splash except NameError: return Qd.InitCursor() time = Evt.TickCount() while _keepsplashscreenopen: ok, event = Evt.EventAvail(highLevelEventMask) if ok: # got apple event, back to mainloop break ok, event = Evt.EventAvail(mDownMask | keyDownMask | updateMask) if ok: ok, event = Evt.WaitNextEvent(mDownMask | keyDownMask | updateMask, 30) if ok: (what, message, when, where, modifiers) = event if what == updateEvt: if Win.WhichWindow(message) == splash: UpdateSplash(1) else: break del splash def about(): global splash, splashresfile, _keepsplashscreenopen _keepsplashscreenopen = 1 splash = Dlg.GetNewDialog(468, -1) splash.DrawDialog() wait() \ No newline at end of file +""" Macstodon - a Mastodon client for classic Mac OS MIT License Copyright (c) 2022-2023 Scott Small and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ """ A quick note from Scott: This file has been almost copied/pasted verbatim from the MacPython IDE, which is why it looks nothing like the other source files style-wise. Changes I made here include: - updated the resources to use the Macstodon logo and give the dialog a black BG - Bugfix to the TextFont calls which were failing and causing the progress bar to not render (the MacPython IDE is affected by this as well, lol) - Fixed the _real__import__ calls which were only passing the name and not the other params, causing crashes when trying to run this in the IDE - Changed the text color to white and changed the text itself - Removed the easter egg """ import Dlg import Res from MacstodonConstants import VERSION splash = Dlg.GetNewDialog(468, -1) splash.DrawDialog() import Qd, TE, Fm, sys _real__import__ = None def install_importhook(): global _real__import__ import __builtin__ if _real__import__ is None: _real__import__ = __builtin__.__import__ __builtin__.__import__ = my__import__ def uninstall_importhook(): global _real__import__ if _real__import__ is not None: import __builtin__ __builtin__.__import__ = _real__import__ _real__import__ = None _progress = 0 def importing(module): global _progress Qd.SetPort(splash) Qd.TextFont(Fm.GetFNum("Geneva")) Qd.TextSize(9) rect = (85, 270, 415, 286) Qd.RGBForeColor((65535,65535,65535)) if module: TE.TETextBox('Importing: ' + module, rect, 0) if not _progress: Qd.FrameRect((85, 286, 415, 294)) pos = min(86 + 330 * _progress / 77, 414) Qd.PaintRect((86, 287, pos, 293)) _progress = _progress + 1 else: Qd.EraseRect(rect) Qd.PaintRect((86, 287, pos, 293)) Qd.RGBForeColor((0,0,0)) def my__import__(name, globals=None, locals=None, fromlist=None): try: return sys.modules[name] except KeyError: try: importing(name) except: try: rv = _real__import__(name, globals, locals, fromlist) finally: uninstall_importhook() return rv return _real__import__(name, globals, locals, fromlist) install_importhook() kHighLevelEvent = 23 import Win from Fonts import * from QuickDraw import * from TextEdit import * import string _keepsplashscreenopen = 0 abouttext1 = """Macstodon %s A basic Mastodon client for Classic Mac OS by Scott Small, @smallsco@oldbytes.space GitHub: https://github.com/smallsco/macstodon Application icon and logo design by MhzModels, @mhzmodeis@artsio.com Additional icon design by CM Harrington, @octothorpe@mastodon.online Built with MacPython %s %s Written by Guido van Rossum with Jack Jansen (and others)""" def nl2return(text): return string.join(string.split(text, '\n'), '\r') def UpdateSplash(drawdialog = 0): if drawdialog: splash.DrawDialog() drawtext() Win.ValidRect(splash.GetWindowPort().portRect) def drawtext(): Qd.SetPort(splash) Qd.TextFont(Fm.GetFNum("Geneva")) Qd.TextSize(9) Qd.RGBForeColor((65535,65535,65535)) rect = (10, 135, 487, 270) import __main__ abouttxt = nl2return(abouttext1 % (VERSION, sys.version, sys.copyright)) TE.TETextBox(abouttxt, rect, teJustCenter) Qd.RGBForeColor((0,0,0)) UpdateSplash(1) def wait(): import Evt from Events import * global splash try: splash except NameError: return Qd.InitCursor() time = Evt.TickCount() while _keepsplashscreenopen: ok, event = Evt.EventAvail(highLevelEventMask) if ok: # got apple event, back to mainloop break ok, event = Evt.EventAvail(mDownMask | keyDownMask | updateMask) if ok: ok, event = Evt.WaitNextEvent(mDownMask | keyDownMask | updateMask, 30) if ok: (what, message, when, where, modifiers) = event if what == updateEvt: if Win.WhichWindow(message) == splash: UpdateSplash(1) else: break del splash def about(): global splash, splashresfile, _keepsplashscreenopen _keepsplashscreenopen = 1 splash = Dlg.GetNewDialog(468, -1) splash.DrawDialog() wait() \ No newline at end of file diff --git a/PrefsWindow.py b/PrefsWindow.py new file mode 100755 index 0000000..cd6ab87 --- /dev/null +++ b/PrefsWindow.py @@ -0,0 +1 @@ +""" Macstodon - a Mastodon client for classic Mac OS MIT License Copyright (c) 2022-2023 Scott Small and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # ############## # Python Imports # ############## import macostools import os import W # ########## # My Imports # ########## from MacstodonConstants import VERSION from MacstodonHelpers import dprint, okDialog # ########### # PrefsWindow # ########### class PrefsWindow(W.ModalDialog): def __init__(self): """ Initializes the PrefsWindow class. """ W.ModalDialog.__init__(self, (200, 200), "Macstodon %s - Preferences" % VERSION) self.setupwidgets() # ######################### # Window Handling Functions # ######################### def setupwidgets(self): """ Defines the prefs window """ app = self.parent prefs = app.getprefs() # debugging dprint("prefs.toots_to_load_startup: %s" % prefs.toots_to_load_startup) dprint("prefs.toots_to_load_refresh: %s" % prefs.toots_to_load_refresh) dprint("prefs.toots_per_timeline: %s" % prefs.toots_per_timeline) dprint("prefs.show_avatars: %s" % prefs.show_avatars) dprint("prefs.show_banners: %s" % prefs.show_banners) # Toots to load at startup self.toots_to_load_startup_text = "Toots to load at startup:\r(0 for none)" self.toots_to_load_startup_label = W.TextBox((10, 10, 160, 32), self.toots_to_load_startup_text) self.toots_to_load_startup_field = W.EditText((170, 10, 24, 16), prefs.toots_to_load_startup) # Toots to load when pressing Refresh in a timeline self.toots_to_load_refresh_text = "Toots to load on refresh:\r(0 for unlimited)" self.toots_to_load_refresh_label = W.TextBox((10, 40, 160, 32), self.toots_to_load_refresh_text) self.toots_to_load_refresh_field = W.EditText((170, 40, 24, 16), prefs.toots_to_load_refresh) # Maximum amount of toots to keep in a timeline self.toots_per_timeline_text = "Max toots per timeline:" self.toots_per_timeline_label = W.TextBox((10, 70, 160, 32), self.toots_per_timeline_text) self.toots_per_timeline_field = W.EditText((170, 70, 24, 16), prefs.toots_per_timeline) # Show/hide images, and clear image cache self.show_avatars_box = W.CheckBox((10, 90, 160, 16), "Show Avatars", None, prefs.show_avatars) self.show_banners_box = W.CheckBox((10, 110, 160, 16), "Show Banners", self.showBannersCallback, prefs.show_banners) self.clear_cache_btn = W.Button((10, 140, 120, 16), "Clear Image Cache", self.clearImageCacheCallback) # Save/cancel changes self.cancel_btn = W.Button((10, -22, 60, 16), "Cancel", self.close) self.save_btn = W.Button((-70, -22, 60, 16), "Save", self.saveButtonCallback) self.setdefaultbutton(self.save_btn) # ################## # Callback Functions # ################## def clearImageCacheCallback(self): """ Run when the user clicks the "Clear Image Cache" button """ app = self.parent cache_acct_list = os.listdir(app.cacheacctfolderpath) for file in cache_acct_list: os.remove(os.path.join(app.cacheacctfolderpath, ":" + file)) cache_media_list = os.listdir(app.cachemediafolderpath) for file in cache_media_list: os.remove(os.path.join(app.cachemediafolderpath, ":" + file)) okDialog("Image cache cleared.") def showBannersCallback(self): """ Run when the user clicks the "Show Banners" checkbox """ if self.show_banners_box.get(): okDialog( "WARNING - while this makes profiles look pretty, it also has a memory leak affecting uncached images that will cause the app to crash eventually." ) def saveButtonCallback(self): """ Run when the user clicks the "Save" button """ app = self.parent prefs = app.getprefs() prefs.toots_to_load_startup = self.toots_to_load_startup_field.get() prefs.toots_to_load_refresh = self.toots_to_load_refresh_field.get() prefs.toots_per_timeline = self.toots_per_timeline_field.get() prefs.show_avatars = self.show_avatars_box.get() prefs.show_banners = self.show_banners_box.get() prefs.save() self.close() \ No newline at end of file diff --git a/ProfileWindow.py b/ProfileWindow.py new file mode 100755 index 0000000..41aeba4 --- /dev/null +++ b/ProfileWindow.py @@ -0,0 +1 @@ +""" Macstodon - a Mastodon client for classic Mac OS MIT License Copyright (c) 2022-2023 Scott Small and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # ############## # Python Imports # ############## import EasyDialogs import ic import Image import Lists import Qd import re import string import time import urllib import urlparse import W # ################### # Third-Party Imports # ################### from third_party.PixMapWrapper import PixMapWrapper # ########## # My Imports # ########## from MacstodonConstants import VERSION from MacstodonHelpers import attachmentsDialog, cleanUpUnicode, dprint, handleRequest, ImageWidget, \ LinkExtractor, linksDialog, okCancelDialog, okDialog, ProfileBanner, ProfilePanel, \ TitledEditText, TimelineList # ########### # Application # ########### class ProfileWindow(W.Window): def __init__(self, account): """ Initializes the ProfileWindow class. """ # Set window size. Default to 400x400 which fits nicely in a 640x480 display. # However if you're on a compact Mac that is 512x342, we need to make it smaller. screenbounds = Qd.qd.screenBits.bounds if screenbounds[3] <= 400: bounds = (0, 20, 400, 342) else: bounds = (400, 400) self.account = account self.acct_name = None self.domain_name = None self.defaulttext = "Click on a toot or notification in one of the above lists..." self.timeline = [] W.Window.__init__(self, bounds, "Macstodon %s - Profile" % VERSION, minsize=(400, 342)) self.setupwidgets() def setupwidgets(self): """ Defines the Profile window. """ prefs = self.parent.getprefs() self.panes = W.HorizontalPanes((8, 8, -8, -20), (0.35, 0.45, 0.2)) self.panes.avgroup = W.Group(None) self.panes.avgroup.banner = ProfileBanner((0, 0, 0, 0), drop_shadow=prefs.show_banners) self.panes.avgroup.avatar = ImageWidget((8, 8, 48, 48)) self.panes.avgroup.note = W.TextBox((-106, 10, 96, 48), "") self.panes.avgroup.note.show(0) self.panes.avgroup.followBtn = W.Button((8, -20, 64, 16), "Follow", callback=self.followCallback) self.panes.avgroup.actionsmenu = W.PopupWidget((80, -20, 16, 16)) self.panes.tlpanes = W.VerticalPanes(None, (0.5, 0.5)) self.panes.tlpanes.info = ProfilePanel(None) self.panes.tlpanes.timeline = TimelineList(None, "Timeline", self.timeline, btnCallback=self.refreshTimelineCallback, callback=self.timelineClickCallback, flags=Lists.lOnlyOne) self.panes.tootgroup = W.Group(None) self.panes.tootgroup.toottxt = TitledEditText((56, 0, -24, 0), title="Toot", text=self.defaulttext, readonly=1, vscroll=1) # Links and Attachment buttons self.panes.tootgroup.links = ImageWidget((-20, 0, 16, 16), pixmap=self.parent.pctLnkDis, callback=self.linksCallback) self.panes.tootgroup.attch = ImageWidget((-20, 18, 16, 16), pixmap=self.parent.pctAtcDis, callback=self.attachmentsCallback) # Reply/boost/favourite/bookmark buttons self.panes.tootgroup.reply = ImageWidget((4, 0, 16, 16), pixmap=self.parent.pctRplDis, callback=self.replyCallback) self.panes.tootgroup.rpnum = W.TextBox((24, 3, 28, 16), "") self.panes.tootgroup.favrt = ImageWidget((4, 18, 16, 16), pixmap=self.parent.pctFvtDis, callback=self.favouriteCallback) self.panes.tootgroup.fvnum = W.TextBox((24, 21, 28, 16), "") self.panes.tootgroup.boost = ImageWidget((4, 36, 16, 16), pixmap=self.parent.pctBstDis, callback=self.boostCallback) self.panes.tootgroup.bonum = W.TextBox((24, 39, 28, 16), "") self.panes.tootgroup.bmark = ImageWidget((4, 54, 16, 16), pixmap=self.parent.pctBkmDis, callback=self.bookmarkCallback) def close(self): del self.parent.profilewindows[self.wid] W.Window.close(self) def open(self): """ Populates an empty profile window with data for a given user. """ W.Window.open(self) self.parent.profilewindows[self.wid] = self self.account["relationship"] = self.getRelationship() if not self.account["relationship"]: self.close() return prefs = self.parent.getprefs() try: W.SetCursor("watch") pb = EasyDialogs.ProgressBar(maxval=10) pb.label("Formatting user name...") pb.inc() display_name = self.account["display_name"] or self.account["username"] display_name = cleanUpUnicode(display_name) acct_split = string.split(self.account["acct"], "@") acct_name = "@%s" % acct_split[0] if len(acct_split) > 1: domain_name = acct_split[1] acct_name_full = "@%s" % self.account["acct"] else: parsed_server = urlparse.urlparse(prefs.server) dprint(parsed_server) domain_name = parsed_server[1] acct_name_full = "%s@%s" % (acct_name, domain_name) self.acct_name = acct_name self.domain_name = domain_name # Window title pb.label("Setting window title...") pb.inc() self.settitle("Macstodon %s - Profile - %s" % (VERSION, acct_name_full)) # Profile banner pb.label("Loading Banner...") pb.inc() self.panes.avgroup.banner.populate(display_name, acct_name_full) if prefs.show_banners: banner = self.parent.imagehandler.getImageFromURL(self.account["header"], "banner") if banner: self.panes.avgroup.banner.setImage(banner) self.panes.avgroup.banner.enable(0) # Follow button label if self.account["relationship"]["following"]: self.panes.avgroup.followBtn.settitle("Unfollow") # Avatar pb.label("Loading Avatar...") pb.inc() if prefs.show_avatars: avatar = self.parent.imagehandler.getImageFromURL(self.account["avatar"], "account") if avatar: self.panes.avgroup.avatar.setImage(avatar) # Note pb.label("Loading Note...") pb.inc() if self.account["relationship"]["note"] != "": self.panes.avgroup.note.show(1) content = self.account["relationship"]["note"] content = string.replace(content, "\n", "\r") self.panes.avgroup.note.set(content) # Stats pb.label("Loading Stats...") pb.inc() self.panes.tlpanes.info.setToots(self.account["statuses_count"]) self.panes.tlpanes.info.setFollowers(self.account["followers_count"]) self.panes.tlpanes.info.setFollowing(self.account["following_count"]) self.panes.tlpanes.info.setLocked(self.account.get("locked", 0)) self.panes.tlpanes.info.setBot(self.account.get("bot", 0)) self.panes.tlpanes.info.setDiscoverable(self.account.get("discoverable", 0)) self.panes.tlpanes.info.setNoIndex(self.account.get("noindex", 0)) self.panes.tlpanes.info.setMoved(self.account.get("moved", 0)) self.panes.tlpanes.info.setSuspended(self.account.get("suspended", 0)) self.panes.tlpanes.info.setLimited(self.account.get("limited", 0)) # Bio pb.label("Loading Bio...") pb.inc() bio = cleanUpUnicode(self.account["note"]) bio = string.replace(bio, "
", "\r") bio = string.replace(bio, "
", "\r") bio = string.replace(bio, "
", "\r") bio = string.replace(bio, "

", "") bio = string.replace(bio, "

", "\r\r") # Extract links in bio bio_le = LinkExtractor() bio_le.feed(bio) bio_le.close() # Strip remaining HTML from bio bio = re.sub('<[^<]+?>', '', bio) self.panes.tlpanes.info.setBio(bio) # Links pb.label("Loading Links...") pb.inc() linksData = [] # Field links for field in self.account["fields"]: name = cleanUpUnicode(field["name"]) if field["verified_at"] is not None: name = "√ " + name value = cleanUpUnicode(field["value"]) field_le = LinkExtractor() field_le.feed(value) field_le.close() for desc, url in field_le.anchors.items(): value = url[0] linksData.append(name + "\r" + value) # Bio links (i.e. hashtags) for desc, url in bio_le.anchors.items(): linksData.append(desc + "\r" + url[0]) self.panes.tlpanes.info.setLinks(linksData) # Interaction Menu pb.label("Building Interaction Menu...") pb.inc() self.buildInteractionMenu() # Cleanup pb.label("Done.") pb.inc() time.sleep(0.5) del pb W.SetCursor("arrow") except KeyboardInterrupt: # the user pressed cancel in the progress bar window W.SetCursor("arrow") self.close() return None # Timeline initial_toots = int(prefs.toots_to_load_startup) if initial_toots: self.refreshTimelineCallback(initial_toots) # ####################### # Menu Handling Functions # ####################### def buildInteractionMenu(self): if self.account["relationship"]["muting"]: muteLabel = "Unmute %s" else: muteLabel = "Mute %s" if self.account["relationship"]["blocking"]: blockLabel = "Unblock %s" else: blockLabel = "Block %s" self.panes.avgroup.actionsmenu.set([ ("Mention %s" % self.acct_name, self.mentionCallback), ("Direct Message %s" % self.acct_name, self.dmCallback), "-", ("Open Original Page", self.openPageCallback), "-", ("Set note for %s" % self.acct_name, self.setNoteCallback), "-", (muteLabel % self.acct_name, self.muteCallback), (blockLabel % self.acct_name, self.blockCallback) ]) def can_logout(self, menuitem): """ Enable the Logout menu item when the Timeline window is open and active. """ return 1 def domenu_logout(self, *args): """ Log out when the Logout menu item is selected. """ self.parent.timelinewindow.close() def can_prefs(self, menuitem): """ Enable the Preferences menu item when the Timeline window is open and active. """ return 1 def domenu_prefs(self, *args): """ Open the Preferences window when the Preferences menu item is selected. """ win = self.parent.PrefsWindow() win.open() # ################## # Callback Functions # ################## def mentionCallback(self): """ Run when "Mention " is selected from the interaction menu """ self.parent.tootwindow = self.parent.TootWindow(replyUser=self.account) self.parent.tootwindow.open() def dmCallback(self): """ Run when "Direct Message " is selected from the interaction menu """ self.parent.tootwindow = self.parent.TootWindow(replyUser=self.account, visibility="direct") self.parent.tootwindow.open() def openPageCallback(self): """ Run when "Open Original Page" is selected from the interaction menu """ prefs = self.parent.getprefs() ic.launchurl("%s/@%s" % (prefs.server, self.account["acct"])) def setNoteCallback(self): """ Run when "Set note for " is selected from the interaction menu """ note = EasyDialogs.AskString( "Please enter a new note for this user, or leave blank to remove any existing note." ) if note is not None: req_data = { "comment": note } path = "/api/v1/accounts/%s/note" % self.account["id"] data = handleRequest(self.parent, path, req_data, use_token=1) if not data: # handleRequest failed and should have popped an error dialog return if data.get("error_description") is not None: okDialog("Server error when setting user note:\r\r %s" % data['error_description']) elif data.get("error") is not None: okDialog("Server error when setting user note:\r\r %s" % data['error']) else: if note != "": self.panes.avgroup.note.show(1) else: self.panes.avgroup.note.show(0) self.panes.avgroup.banner.enable(1) self.panes.avgroup.banner.enable(0) self.panes.avgroup.actionsmenu.draw() self.panes.avgroup.avatar.draw() self.panes.avgroup.note.set(note) def muteCallback(self): """ Run when "Mute " is selected from the interaction menu """ if self.account["relationship"]["muting"]: # already muting, undo it action = "unmute" else: # not muting yet action = "mute" try: okCancelDialog("Are you sure you want to mute @%s indefinitely?" % self.account["acct"]) except KeyboardInterrupt: return path = "/api/v1/accounts/%s/%s" % (self.account["id"], action) data = handleRequest(self.parent, path, {}, use_token=1) if not data: # handleRequest failed and should have popped an error dialog return if data.get("error_description") is not None: okDialog("Server error when trying to %s user:\r\r %s" % (action, data['error_description'])) elif data.get("error") is not None: okDialog("Server error when trying to %s user:\r\r %s" % (action, data['error'])) else: self.account["relationship"] = data self.buildInteractionMenu() okDialog("User %sd successfully!" % action) def blockCallback(self): """ Run when "Block " is selected from the interaction menu """ if self.account["relationship"]["blocking"]: # already blocking, undo it action = "unblock" else: # not blocking yet action = "block" try: okCancelDialog("Are you sure you want to block @%s?" % self.account["acct"]) except KeyboardInterrupt: return path = "/api/v1/accounts/%s/%s" % (self.account["id"], action) data = handleRequest(self.parent, path, {}, use_token=1) if not data: # handleRequest failed and should have popped an error dialog return if data.get("error_description") is not None: okDialog("Server error when %sing user:\r\r %s" % (action, data['error_description'])) elif data.get("error") is not None: okDialog("Server error when %sing user:\r\r %s" % (action, data['error'])) else: self.account["relationship"] = data self.buildInteractionMenu() okDialog("User %sed successfully!" % action) def followCallback(self): """ Run when the user clicks the Follow (Unfollow) button """ if self.account["relationship"]["following"]: # already following, undo it action = "unfollow" else: # not following yet action = "follow" path = "/api/v1/accounts/%s/%s" % (self.account["id"], action) data = handleRequest(self.parent, path, {}, use_token=1) if not data: # handleRequest failed and should have popped an error dialog return if data.get("error_description") is not None: okDialog("Server error when %sing user:\r\r %s" % (action, data['error_description'])) elif data.get("error") is not None: okDialog("Server error when %sing user:\r\r %s" % (action, data['error'])) else: self.account["relationship"] = data okDialog("User %sed successfully!" % action) if action == "follow": self.panes.avgroup.followBtn.settitle("Unfollow") else: self.panes.avgroup.followBtn.settitle("Follow") def timelineClickCallback(self): """ Run when the user clicks somewhere in the named timeline """ list = self.panes.tlpanes.timeline selected = list.getselection() if len(selected) < 1: self.panes.tootgroup.reply.setImage(self.parent.pctRplDis) self.panes.tootgroup.favrt.setImage(self.parent.pctFvtDis) self.panes.tootgroup.boost.setImage(self.parent.pctBstDis) self.panes.tootgroup.bmark.setImage(self.parent.pctBkmDis) self.panes.tootgroup.links.setImage(self.parent.pctLnkDis) self.panes.tootgroup.attch.setImage(self.parent.pctAtcDis) self.panes.tootgroup.fvnum.set("") self.panes.tootgroup.bonum.set("") self.panes.tootgroup.rpnum.set("") self.panes.tootgroup.toottxt.setTitle("") self.panes.tootgroup.toottxt.set(self.defaulttext) return else: index = selected[0] toot = self.timeline[index] self.formatAndDisplayToot(toot) def refreshTimelineCallback(self, limit=None): """ Run when the user clicks the Refresh button above the timeline """ self.updateTimeline(limit) self.panes.tlpanes.timeline.set(self.formatTimelineForList()) def replyCallback(self): """ Run when the user clicks the "Reply" button from the timeline window. It opens up the toot window, passing the currently selected toot as a parameter. """ toot, origToot, timeline, index = self.getSelectedToot(resolve_boosts=1) if toot: self.parent.tootwindow = self.parent.TootWindow(replyTo=toot) self.parent.tootwindow.open() else: okDialog("Please select a toot first.") def boostCallback(self): """ Boosts a toot. Removes the boost if the toot was already boosted. """ toot, origToot, timeline, index = self.getSelectedToot(resolve_boosts=1) if toot: if toot["reblogged"]: # already boosted, undo it action = "unreblog" else: # not bookmarked yet action = "reblog" visibility = toot["visibility"] if visibility == "limited" or visibility == "direct": visibility = "public" req_data = { "visibility": visibility } path = "/api/v1/statuses/%s/%s" % (toot["id"], action) data = handleRequest(self.parent, path, req_data, use_token=1) if not data: # handleRequest failed and should have popped an error dialog return if data.get("error_description") is not None: okDialog("Server error when %sing toot:\r\r %s" % (action, data['error_description'])) elif data.get("error") is not None: okDialog("Server error when %sing toot:\r\r %s" % (action, data['error'])) else: if origToot: if origToot.get("reblog"): dprint("overwriting boosted toot") timeline[index]["reblog"] = data["reblog"] else: dprint("overwriting notification") timeline[index]["status"] = data["reblog"] else: dprint("overwriting normal toot") timeline[index] = data["reblog"] okDialog("Toot %sged successfully!" % action) if action == "reblog": self.panes.tootgroup.boost.setImage(self.parent.pctBstClr) else: self.panes.tootgroup.boost.setImage(self.parent.pctBstBnW) else: okDialog("Please select a toot first.") def favouriteCallback(self): """ Favourites a toot. Removes the favourite if the toot was already favourited. """ toot, origToot, timeline, index = self.getSelectedToot(resolve_boosts=1) if toot: if toot["favourited"]: # already favourited, undo it action = "unfavourite" else: # not favourited yet action = "favourite" path = "/api/v1/statuses/%s/%s" % (toot["id"], action) data = handleRequest(self.parent, path, {}, use_token=1) if not data: # handleRequest failed and should have popped an error dialog return if data.get("error_description") is not None: okDialog("Server error when %sing toot:\r\r %s" % (action, data['error_description'])) elif data.get("error") is not None: okDialog("Server error when %sing toot:\r\r %s" % (action, data['error'])) else: if origToot: if origToot.get("reblog"): dprint("overwriting boosted toot") timeline[index]["reblog"] = data else: dprint("overwriting notification") timeline[index]["status"] = data else: dprint("overwriting normal toot") timeline[index] = data okDialog("Toot %sd successfully!" % action) if action == "favourite": self.panes.tootgroup.favrt.setImage(self.parent.pctFvtClr) else: self.panes.tootgroup.favrt.setImage(self.parent.pctFvtBnW) else: okDialog("Please select a toot first.") def bookmarkCallback(self): """ Bookmarks a toot. Removes the bookmark if the toot was already bookmarked. """ toot, origToot, timeline, index = self.getSelectedToot(resolve_boosts=1) if toot: if toot["bookmarked"]: # already bookmarked, undo it action = "unbookmark" else: # not bookmarked yet action = "bookmark" path = "/api/v1/statuses/%s/%s" % (toot["id"], action) data = handleRequest(self.parent, path, {}, use_token=1) if not data: # handleRequest failed and should have popped an error dialog return if data.get("error_description") is not None: okDialog("Server error when %sing toot:\r\r %s" % (action, data['error_description'])) elif data.get("error") is not None: okDialog("Server error when %sing toot:\r\r %s" % (action, data['error'])) else: if origToot: if origToot.get("reblog"): dprint("overwriting boosted toot") timeline[index]["reblog"] = data else: dprint("overwriting notification") timeline[index]["status"] = data else: dprint("overwriting normal toot") timeline[index] = data okDialog("Toot %sed successfully!" % action) if action == "bookmark": self.panes.tootgroup.favrt.setImage(self.parent.pctFvtClr) else: self.panes.tootgroup.favrt.setImage(self.parent.pctFvtBnW) else: okDialog("Please select a toot first.") def linksCallback(self): """ Displays a dialog containing the links in the toot and allows the user to open them. """ toot, origToot, timeline, index = self.getSelectedToot(resolve_boosts=1) if toot: content = toot["content"] # Replace HTML linebreak tags with actual linebreaks content = cleanUpUnicode(content) # Extract links le = LinkExtractor() le.feed(content) le.close() linksDialog(le) else: okDialog("Please select a toot first.") def attachmentsCallback(self): toot, origToot, timeline, index = self.getSelectedToot(resolve_boosts=1) if toot: attachmentsDialog(toot["media_attachments"]) else: okDialog("Please select a toot first.") # #################### # Formatting Functions # #################### def formatAndDisplayToot(self, toot): """ Formats a toot for display and displays it in the bottom third """ # clear existing toot self.panes.tootgroup.reply.setImage(self.parent.pctRplDis) self.panes.tootgroup.favrt.setImage(self.parent.pctFvtDis) self.panes.tootgroup.boost.setImage(self.parent.pctBstDis) self.panes.tootgroup.bmark.setImage(self.parent.pctBkmDis) self.panes.tootgroup.links.setImage(self.parent.pctLnkDis) self.panes.tootgroup.attch.setImage(self.parent.pctAtcDis) self.panes.tootgroup.fvnum.set("") self.panes.tootgroup.bonum.set("") self.panes.tootgroup.rpnum.set("") self.panes.tootgroup.toottxt.setTitle("") self.panes.tootgroup.toottxt.set("Loading toot...") display_name = toot["account"]["display_name"] or toot["account"]["username"] display_name = cleanUpUnicode(display_name) if toot["reblog"]: reblog_display_name = toot["reblog"]["account"]["display_name"] or toot["reblog"]["account"]["username"] reblog_display_name = cleanUpUnicode(reblog_display_name) title = "%s boosted %s (@%s)" % (display_name, reblog_display_name, toot["reblog"]["account"]["acct"]) content = toot["reblog"]["content"] sensitive = toot["reblog"]["sensitive"] spoiler_text = toot["reblog"]["spoiler_text"] favourites_count = toot["reblog"]["favourites_count"] reblogs_count = toot["reblog"]["reblogs_count"] replies_count = toot["reblog"]["replies_count"] favourited = toot["reblog"]["favourited"] reblogged = toot["reblog"]["reblogged"] bookmarked = toot["reblog"]["bookmarked"] else: title = "%s (@%s)" % (display_name, toot["account"]["acct"]) content = toot["content"] sensitive = toot["sensitive"] spoiler_text = toot["spoiler_text"] favourites_count = toot["favourites_count"] reblogs_count = toot["reblogs_count"] replies_count = toot["replies_count"] favourited = toot["favourited"] reblogged = toot["reblogged"] bookmarked = toot["bookmarked"] # Check for CW if sensitive: cwText = "This toot has a content warning. " \ "Press OK to view or Cancel to not view.\r\r%s" try: okCancelDialog(cwText % spoiler_text) except KeyboardInterrupt: self.panes.tootgroup.toottxt.set(self.defaulttext) return # Replace HTML linebreak tags with actual linebreaks content = cleanUpUnicode(content) content = string.replace(content, "
", "\r") content = string.replace(content, "
", "\r") content = string.replace(content, "
", "\r") content = string.replace(content, "

", "") content = string.replace(content, "

", "\r\r") # Extract links le = LinkExtractor() le.feed(content) le.close() # Strip all other HTML tags content = re.sub('<[^<]+?>', '', content) # Render content into UI self.panes.tootgroup.reply.setImage(self.parent.pctRplBnW) if favourited: self.panes.tootgroup.favrt.setImage(self.parent.pctFvtClr) else: self.panes.tootgroup.favrt.setImage(self.parent.pctFvtBnW) if reblogged: self.panes.tootgroup.boost.setImage(self.parent.pctBstClr) else: self.panes.tootgroup.boost.setImage(self.parent.pctBstBnW) if bookmarked: self.panes.tootgroup.bmark.setImage(self.parent.pctBkmClr) else: self.panes.tootgroup.bmark.setImage(self.parent.pctBkmBnW) self.panes.tootgroup.links.setImage(self.parent.pctLnkBnW) self.panes.tootgroup.attch.setImage(self.parent.pctAtcBnW) self.panes.tootgroup.toottxt.setTitle(title) self.panes.tootgroup.toottxt.set(content) self.panes.tootgroup.fvnum.set(str(favourites_count)) self.panes.tootgroup.bonum.set(str(reblogs_count)) self.panes.tootgroup.rpnum.set(str(replies_count)) def formatTimelineForList(self): """ Formats toots for display in a timeline list """ listitems = [] for toot in self.timeline: if toot["reblog"]: if toot["reblog"]["sensitive"]: content = toot["reblog"]["spoiler_text"] else: content = toot["reblog"]["content"] else: if toot["sensitive"]: content = toot["spoiler_text"] else: content = toot["content"] content = cleanUpUnicode(content) # Replace linebreaks with spaces content = string.replace(content, "
", " ") content = string.replace(content, "
", " ") content = string.replace(content, "
", " ") content = string.replace(content, "

", "") content = string.replace(content, "

", " ") # Strip all other HTML tags content = re.sub('<[^<]+?>', '', content) display_name = toot["account"]["display_name"] or toot["account"]["username"] display_name = cleanUpUnicode(display_name) if toot["reblog"]: reblog_display_name = toot["reblog"]["account"]["display_name"] or toot["reblog"]["account"]["username"] reblog_display_name = cleanUpUnicode(reblog_display_name) listitem = "%s boosted %s\r%s" % (display_name, reblog_display_name, content) else: listitem = "%s\r%s" % (display_name, content) listitems.append(listitem) return listitems # ################ # Helper Functions # ################ def getRelationship(self): """ Returns properties related to the relationship between the logged-in user and the user whos profile is being viewed """ # can't use urllib here because py1.5.2 doesn't support the [] syntax path = "/api/v1/accounts/relationships?id[]=" + self.account["id"] data = handleRequest(self.parent, path, use_token=1) if not data: # handleRequest failed and should have popped an error dialog return 0 # if data is a list, it worked if type(data) == type([]): return data[0] # if data is a dict, it failed elif type(data) == type({}) and data.get("error") is not None: okDialog("Server error when reading account relationship:\r\r %s" % data['error']) return 0 # i don't think this is reachable, but just in case... else: okDialog("Server error when reading account relationship. Unable to determine data type.") return 0 def getSelectedToot(self, resolve_boosts=0): """ Returns the selected toot, the containing toot (if boost or notification), the timeline to which the toot belongs, and the index of the toot in the timeline. """ timeline = self.panes.tlpanes.timeline selected = timeline.getselection() if len(selected) > 0: index = selected[0] toot = self.timeline[index] timeline = self.timeline else: return None, None, None, None if toot.get("reblog") and resolve_boosts: return toot["reblog"], toot, timeline, index else: return toot, None, timeline, index def updateTimeline(self, limit = None): """ Pulls a timeline from the server and updates the global dict """ params = {} app = self.parent prefs = app.getprefs() if limit: # If a limit was explicitly set in the call, use that params["limit"] = limit else: # Otherwise, use the refresh limit from the prefs if one was set refresh_toots = int(prefs.toots_to_load_refresh) if refresh_toots: params["limit"] = refresh_toots if len(self.timeline) > 0: params["min_id"] = self.timeline[0]["id"] path = "/api/v1/accounts/%s/statuses" % self.account["id"] encoded_params = urllib.urlencode(params) if encoded_params: path = path + "?" + encoded_params data = handleRequest(self.parent, path, use_token=1) if not data: # handleRequest failed and should have popped an error dialog return # if data is a list, it worked if type(data) == type([]): for i in range(len(data)-1, -1, -1): self.timeline.insert(0, data[i]) self.timeline = self.timeline[:int(prefs.toots_per_timeline)] # if data is a dict, it failed elif type(data) == type({}) and data.get("error") is not None: okDialog("Server error when refreshing timeline:\r\r %s" % data['error']) # i don't think this is reachable, but just in case... else: okDialog("Server error when refreshing timeline. Unable to determine data type.") \ No newline at end of file diff --git a/README.md b/README.md old mode 100644 new mode 100755 index c84a13c..1b4c3be --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Macstodon is an app written in MacPython 1.5.2 for Classic Mac OS that lets you System Requirements are: * A 68k Macintosh with a 68020, 68030, or 68040 processor, or, any Power Macintosh -* At least 1.5 MB of free memory (more if you want to be able to view avatars) +* At least 1.5 MB of free memory (4 MB to view avatars, 8 MB to view banners) * System 7.1 to Mac OS 9.2.2 * 32-bit addressing enabled * Internet Config installed if you are running Mac OS 8.1 or earlier @@ -25,12 +25,24 @@ The following extensions are required for System 7 users, and can be found in th

Timeline View

+

+ Profile View +

Replying to a Toot

Content Warning Dialog

+

+ Viewing Links in a Toot +

+

+ Viewing Attachments in a Toot +

+

+ Editing Preferences +

## Features @@ -40,6 +52,10 @@ The following extensions are required for System 7 users, and can be found in th * View your notifications * Favourite, boost, and bookmark toots * Reply to toots from others +* Follow links in toots +* Download attachments from toots +* Look up users by handle and view their profiles +* Follow/unfollow, mute/unmute, and block/unblock users That's it for now. Maybe more features will be implemented in a later version. @@ -89,10 +105,10 @@ That's it for now. Maybe more features will be implemented in a later version. Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.2 Safari/605.1.15 ``` * You will need to use **http** instead of **https** in the server URL for Macstodon. This is a limitation of the *urllib* library in MacPython 1.5.2. -* There is no support for Unicode whatsoever, and there never will be. Toots or usernames with emojis and special characters in them will look funny. +* There is no support for Unicode whatsoever, and there never will be. Toots or usernames with emojis and special characters in them will have those characters removed. * If Macstodon actually crashes or unexpectedly quits while loading data from the server, try allocating more memory to it using the Get Info screen in the Finder. -* If the `Timeline` window is closed, you can't get it back and will have to quit Macstodon from the File menu and relaunch it. * If images (avatars) fail to load, but the rest of the app seems to be working just fine, this means you need to give Macstodon more memory. Allocating more memory to it using the Get Info screen in the Finder will resolve this issue (you should also remove the image cache, see below) +* There is a nasty memory leak around the loading of uncached banner images. Enabling the option to view banner images will cause Macstodon to run out of memory and crash pretty quickly. ## Troubleshooting When in doubt, delete the preferences file. It is named `Macstodon Prefs` and lives in the Preferences folder in your System Folder. Deleting the preferences file will make Macstodon forget about the saved server, tokens, etc. @@ -100,14 +116,8 @@ When in doubt, delete the preferences file. It is named `Macstodon Prefs` and li There is also a subfolder of the Preferences folder named Macstodon Cache. This folder contains avatars and other images that have been resized, so we don't need to download and resize them again the next time we encounter them in the wild. Occasionally this can become corrupted and an original image can be cached instead of a resized one, leading to poor performance and high memory usage. If this happens, you can delete this folder, it will be recreated on the next launch. ## Credits -Special thanks to the following third-party software, for whom without Macstodon would not be possible: - -**BeautifulSoup pre-1.3** -Copyright ©2004 Leonard Richardson -License: Python - - -Extra special thanks to: +Special thanks to the following people, for whom without Macstodon would not be possible: [Dan](https://bitbang.social/@billgoats) - for the inspiration to work on this project [Mingo](https://oldbytes.space/@mingo) - for [suggesting the name](https://oldbytes.space/@mingo/109316322622806248) -MhzModels - for the beautiful logo at the top of this README, and the Macstodon application icon! +[MhzModels](https://artsio.com/@mhzmodels) - for the beautiful logo at the top of this README, and the application icon +[CM Harrington](https://mastodon.online/@octothorpe) - for additional icon design diff --git a/TimelineHandler.py b/TimelineHandler.py deleted file mode 100644 index 0bec918..0000000 --- a/TimelineHandler.py +++ /dev/null @@ -1 +0,0 @@ -""" Macstodon - a Mastodon client for classic Mac OS MIT License Copyright (c) 2022-2023 Scott Small and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # ############## # Python Imports # ############## import Lists import Qd import re import string import urllib import W # ################### # Third-Party Imports # ################### from third_party.BeautifulSoup import BeautifulSoup # ########## # My Imports # ########## from MacstodonConstants import VERSION from MacstodonHelpers import cleanUpUnicode, dprint, handleRequest, ImageWidget, okDialog, \ okCancelDialog, TitledEditText, TimelineList # ########### # Application # ########### class TimelineHandler: def __init__(self, app): """ Initializes the TimelineHandler class. """ self.app = app self.defaulttext = "Click on a toot or notification in one of the above lists..." self.timelines = { "home": [], "local": [], "notifications": [] } # ######################### # Window Handling Functions # ######################### def getTimelineWindow(self): """ Defines the Timeline window """ # Set window size. Default to 600x400 which fits nicely in a 640x480 display. # However if you're on a compact Mac that is 512x342, we need to make it smaller. screenbounds = Qd.qd.screenBits.bounds if screenbounds[2] <= 600 and screenbounds[3] <= 400: bounds = (0, 20, 512, 342) else: bounds = (600, 400) w = W.Window(bounds, "Macstodon %s - Timeline" % VERSION, minsize=(512, 342)) w.panes = W.HorizontalPanes((8, 8, -8, -20), (0.6, 0.4)) w.panes.tlpanes = W.VerticalPanes(None, (0.34, 0.33, 0.33)) w.panes.tlpanes.home = TimelineList(None, "Home Timeline", self.timelines["home"], btnCallback=self.refreshHomeCallback, callback=self.homeClickCallback, flags=Lists.lOnlyOne) w.panes.tlpanes.local = TimelineList(None, "Local Timeline", self.timelines["local"], btnCallback=self.refreshLocalCallback, callback=self.localClickCallback, flags=Lists.lOnlyOne) w.panes.tlpanes.notifications = TimelineList(None, "Notifications", self.timelines["notifications"], btnCallback=self.refreshNotificationsCallback, callback=self.notificationClickCallback, flags=Lists.lOnlyOne) w.panes.tootgroup = W.Group(None) w.panes.tootgroup.toottxt = TitledEditText((56, 0, 0, -20), title="", text=self.defaulttext, readonly=1, vscroll=1) # Avatar, reply/boost/favourite/bookmark buttons w.panes.tootgroup.authorimg = ImageWidget((0, 0, 48, 48)) w.panes.tootgroup.boosterimg = ImageWidget((24, 24, 24, 24)) w.panes.tootgroup.reply = W.Button((4, 52, 16, 16), "R", self.replyCallback) w.panes.tootgroup.rpnum = W.TextBox((24, 55, 28, 16), "") w.panes.tootgroup.favrt = W.Button((4, 70, 16, 16), "F", self.favouriteCallback) w.panes.tootgroup.fvnum = W.TextBox((24, 73, 28, 16), "") w.panes.tootgroup.boost = W.Button((4, 88, 16, 16), "B", self.boostCallback) w.panes.tootgroup.bonum = W.TextBox((24, 91, 28, 16), "") w.panes.tootgroup.bmark = W.Button((4, 106, 16, 16), "M", self.bookmarkCallback) w.panes.tootgroup.logoutbutton = W.Button((56, -15, 80, 0), "Logout", self.timelineLogoutCallback) w.panes.tootgroup.tootbutton = W.Button((-80, -15, 80, 0), "Post Toot", self.tootCallback) return w # ################## # Callback Functions # ################## def timelineClickCallback(self, name): """ Run when the user clicks somewhere in the named timeline """ w = self.app.timelinewindow if name == "home": list = w.panes.tlpanes.home elif name == "local": list = w.panes.tlpanes.local selected = list.getselection() if len(selected) < 1: w.panes.tootgroup.authorimg.clearImage() w.panes.tootgroup.boosterimg.clearImage() w.panes.tootgroup.fvnum.set("") w.panes.tootgroup.bonum.set("") w.panes.tootgroup.rpnum.set("") w.panes.tootgroup.toottxt.setTitle("") w.panes.tootgroup.toottxt.set(self.defaulttext) return else: index = selected[0] toot = self.timelines[name][index] self.formatAndDisplayToot(toot) def homeClickCallback(self): """ Run when the user clicks somewhere in the home timeline """ w = self.app.timelinewindow w.panes.tlpanes.local.setselection([-1]) w.panes.tlpanes.notifications.setselection([-1]) self.timelineClickCallback("home") def localClickCallback(self): """ Run when the user clicks somewhere in the local timeline """ w = self.app.timelinewindow w.panes.tlpanes.home.setselection([-1]) w.panes.tlpanes.notifications.setselection([-1]) self.timelineClickCallback("local") def notificationClickCallback(self): """ Run when the user clicks somewhere in the notification timeline """ w = self.app.timelinewindow w.panes.tlpanes.home.setselection([-1]) w.panes.tlpanes.local.setselection([-1]) list = w.panes.tlpanes.notifications selected = list.getselection() if len(selected) < 1: w.panes.tootgroup.authorimg.clearImage() w.panes.tootgroup.boosterimg.clearImage() w.panes.tootgroup.fvnum.set("") w.panes.tootgroup.bonum.set("") w.panes.tootgroup.rpnum.set("") w.panes.tootgroup.toottxt.setTitle("") w.panes.tootgroup.toottxt.set(self.defaulttext) return else: index = selected[0] notification = self.timelines["notifications"][index] if notification["type"] in ["favourite", "reblog", "status", "mention", "poll", "update"]: toot = notification["status"] self.formatAndDisplayToot(toot) else: okDialog("Sorry, displaying the notification type '%s' is not supported yet" % notification["type"]) w.panes.tootgroup.authorimg.clearImage() w.panes.tootgroup.boosterimg.clearImage() w.panes.tootgroup.fvnum.set("") w.panes.tootgroup.bonum.set("") w.panes.tootgroup.rpnum.set("") w.panes.tootgroup.toottxt.setTitle("") w.panes.tootgroup.toottxt.set(self.defaulttext) def refreshHomeCallback(self, limit=None): """ Run when the user clicks the Refresh button above the home timeline """ self.updateTimeline("home", limit) self.app.timelinewindow.panes.tlpanes.home.set(self.formatTimelineForList("home")) def refreshLocalCallback(self, limit=None): """ Run when the user clicks the Refresh button above the local timeline """ self.updateTimeline("local", limit) self.app.timelinewindow.panes.tlpanes.local.set(self.formatTimelineForList("local")) def refreshNotificationsCallback(self, limit=None): """ Run when the user clicks the Refresh button above the notifications timeline """ self.updateTimeline("notifications", limit) listitems = self.formatNotificationsForList() self.app.timelinewindow.panes.tlpanes.notifications.set(listitems) def timelineLogoutCallback(self): """ Run when the user clicks the "Logout" button from the timeline window. Just closes the timeline window and reopens the login window. """ self.app.timelinewindow.close() self.app.loginwindow = self.app.authhandler.getLoginWindow() self.app.loginwindow.open() def tootCallback(self): """ Run when the user clicks the "Post Toot" button from the timeline window. It opens up the toot window. """ self.app.tootwindow = self.app.toothandler.getTootWindow() self.app.tootwindow.open() def replyCallback(self): """ Run when the user clicks the "Reply" button from the timeline window. It opens up the toot window, passing the currently selected toot as a parameter. """ toot, origToot, timeline, index = self.getSelectedToot(resolve_boosts=1) if toot: self.app.tootwindow = self.app.toothandler.getTootWindow(replyTo=toot) self.app.tootwindow.open() else: okDialog("Please select a toot first.") def boostCallback(self): """ Boosts a toot. Removes the boost if the toot was already boosted. """ toot, origToot, timeline, index = self.getSelectedToot(resolve_boosts=1) if toot: if toot["reblogged"]: # already boosted, undo it action = "unreblog" else: # not bookmarked yet action = "reblog" visibility = toot["visibility"] if visibility == "limited" or visibility == "direct": visibility = "public" req_data = { "visibility": visibility } path = "/api/v1/statuses/%s/%s" % (toot["id"], action) data = handleRequest(self.app, path, req_data, use_token=1) if not data: # handleRequest failed and should have popped an error dialog return if data.get("error_description") is not None: okDialog("Server error when %sing toot:\r\r %s" % (action, data['error_description'])) elif data.get("error") is not None: okDialog("Server error when %sing toot:\r\r %s" % (action, data['error'])) else: if origToot: if origToot.get("reblog"): dprint("overwriting boosted toot") timeline[index]["reblog"] = data["reblog"] else: dprint("overwriting notification") timeline[index]["status"] = data["reblog"] else: dprint("overwriting normal toot") timeline[index] = data["reblog"] okDialog("Toot %sged successfully!" % action) else: okDialog("Please select a toot first.") def favouriteCallback(self): """ Favourites a toot. Removes the favourite if the toot was already favourited. """ toot, origToot, timeline, index = self.getSelectedToot(resolve_boosts=1) if toot: if toot["favourited"]: # already favourited, undo it action = "unfavourite" else: # not favourited yet action = "favourite" path = "/api/v1/statuses/%s/%s" % (toot["id"], action) data = handleRequest(self.app, path, {}, use_token=1) if not data: # handleRequest failed and should have popped an error dialog return if data.get("error_description") is not None: okDialog("Server error when %sing toot:\r\r %s" % (action, data['error_description'])) elif data.get("error") is not None: okDialog("Server error when %sing toot:\r\r %s" % (action, data['error'])) else: if origToot: if origToot.get("reblog"): dprint("overwriting boosted toot") timeline[index]["reblog"] = data else: dprint("overwriting notification") timeline[index]["status"] = data else: dprint("overwriting normal toot") timeline[index] = data okDialog("Toot %sd successfully!" % action) else: okDialog("Please select a toot first.") def bookmarkCallback(self): """ Bookmarks a toot. Removes the bookmark if the toot was already bookmarked. """ toot, origToot, timeline, index = self.getSelectedToot(resolve_boosts=1) if toot: if toot["bookmarked"]: # already bookmarked, undo it action = "unbookmark" else: # not bookmarked yet action = "bookmark" path = "/api/v1/statuses/%s/%s" % (toot["id"], action) data = handleRequest(self.app, path, {}, use_token=1) if not data: # handleRequest failed and should have popped an error dialog return if data.get("error_description") is not None: okDialog("Server error when %sing toot:\r\r %s" % (action, data['error_description'])) elif data.get("error") is not None: okDialog("Server error when %sing toot:\r\r %s" % (action, data['error'])) else: if origToot: if origToot.get("reblog"): dprint("overwriting boosted toot") timeline[index]["reblog"] = data else: dprint("overwriting notification") timeline[index]["status"] = data else: dprint("overwriting normal toot") timeline[index] = data okDialog("Toot %sed successfully!" % action) else: okDialog("Please select a toot first.") # #################### # Formatting Functions # #################### def formatAndDisplayToot(self, toot): """ Formats a toot for display and displays it in the bottom third """ w = self.app.timelinewindow # clear existing toot w.panes.tootgroup.authorimg.clearImage() w.panes.tootgroup.boosterimg.clearImage() w.panes.tootgroup.fvnum.set("") w.panes.tootgroup.bonum.set("") w.panes.tootgroup.rpnum.set("") w.panes.tootgroup.toottxt.setTitle("") w.panes.tootgroup.toottxt.set("Loading toot...") display_name = toot["account"]["display_name"] or toot["account"]["username"] display_name = cleanUpUnicode(display_name) if toot["reblog"]: image = self.app.imagehandler.getImageFromURL(toot["reblog"]["account"]["avatar"], "account") bimage = self.app.imagehandler.getImageFromURL(toot["account"]["avatar"], "account") reblog_display_name = toot["reblog"]["account"]["display_name"] or toot["reblog"]["account"]["username"] reblog_display_name = cleanUpUnicode(reblog_display_name) title = "%s boosted %s (@%s)" % (display_name, reblog_display_name, toot["reblog"]["account"]["acct"]) content = toot["reblog"]["content"] sensitive = toot["reblog"]["sensitive"] spoiler_text = toot["reblog"]["spoiler_text"] favourites_count = toot["reblog"]["favourites_count"] reblogs_count = toot["reblog"]["reblogs_count"] replies_count = toot["reblog"]["replies_count"] else: image = self.app.imagehandler.getImageFromURL(toot["account"]["avatar"], "account") bimage = None title = "%s (@%s)" % (display_name, toot["account"]["acct"]) content = toot["content"] sensitive = toot["sensitive"] spoiler_text = toot["spoiler_text"] favourites_count = toot["favourites_count"] reblogs_count = toot["reblogs_count"] replies_count = toot["replies_count"] # Check for CW if sensitive: cwText = "This toot has a content warning. " \ "Press OK to view or Cancel to not view.\r\r%s" try: okCancelDialog(cwText % spoiler_text) except KeyboardInterrupt: w.panes.tootgroup.toottxt.set(self.defaulttext) return # Replace HTML linebreak tags with actual linebreaks content = cleanUpUnicode(content) content = string.replace(content, "
", "\r") content = string.replace(content, "
", "\r") content = string.replace(content, "
", "\r") content = string.replace(content, "

", "") content = string.replace(content, "

", "\r\r") # Extract links #soup = BeautifulSoup(content) #links = soup("a") #dprint("** ANCHORS **") #dprint(links) # Strip all other HTML tags content = re.sub('<[^<]+?>', '', content) # Render content into UI if image: w.panes.tootgroup.authorimg.setImage(image) if bimage: w.panes.tootgroup.boosterimg.setImage(bimage) w.panes.tootgroup.toottxt.setTitle(title) w.panes.tootgroup.toottxt.set(content) w.panes.tootgroup.fvnum.set(str(favourites_count)) w.panes.tootgroup.bonum.set(str(reblogs_count)) w.panes.tootgroup.rpnum.set(str(replies_count)) def formatTimelineForList(self, name): """ Formats toots for display in a timeline list """ listitems = [] for toot in self.timelines[name]: if toot["reblog"]: if toot["reblog"]["sensitive"]: content = toot["reblog"]["spoiler_text"] else: content = toot["reblog"]["content"] else: if toot["sensitive"]: content = toot["spoiler_text"] else: content = toot["content"] content = cleanUpUnicode(content) # Replace linebreaks with spaces content = string.replace(content, "
", " ") content = string.replace(content, "
", " ") content = string.replace(content, "
", " ") content = string.replace(content, "

", "") content = string.replace(content, "

", " ") # Strip all other HTML tags content = re.sub('<[^<]+?>', '', content) display_name = toot["account"]["display_name"] or toot["account"]["username"] display_name = cleanUpUnicode(display_name) if toot["reblog"]: reblog_display_name = toot["reblog"]["account"]["display_name"] or toot["reblog"]["account"]["username"] reblog_display_name = cleanUpUnicode(reblog_display_name) listitem = "%s boosted %s\r%s" % (display_name, reblog_display_name, content) else: listitem = "%s\r%s" % (display_name, content) listitems.append(listitem) return listitems def formatNotificationsForList(self): """ Formats notifications for display in a list """ listitems = [] for notification in self.timelines["notifications"]: display_name = notification["account"]["display_name"] or notification["account"]["username"] if notification["type"] == "mention": listitem = "%s mentioned you in their toot" % display_name elif notification["type"] == "status": listitem = "%s posted a toot" % display_name elif notification["type"] == "reblog": listitem = "%s boosted your toot" % display_name elif notification["type"] == "follow": listitem = "%s followed you" % display_name elif notification["type"] == "follow_request": listitem = "%s requested to follow you" % display_name elif notification["type"] == "favourite": listitem = "%s favourited your toot" % display_name elif notification["type"] == "poll": listitem = "%s's poll has ended" % display_name elif notification["type"] == "update": listitem = "%s updated their toot" % display_name elif notification["type"] == "admin.sign_up": listitem = "%s signed up" % display_name elif notification["type"] == "admin.report": listitem = "%s filed a report" % display_name else: # unknown type, ignore it, but print to console if debugging dprint("Unknown notification type: %s" % notification["type"]) listitems.append(listitem) return listitems # ################ # Helper Functions # ################ def getSelectedToot(self, resolve_boosts=0): """ Returns the selected toot, the containing toot (if boost or notification), the timeline to which the toot belongs, and the index of the toot in the timeline. """ w = self.app.timelinewindow homeTimeline = w.panes.tlpanes.home localTimeline = w.panes.tlpanes.local notificationsTimeline = w.panes.tlpanes.notifications homeSelected = homeTimeline.getselection() localSelected = localTimeline.getselection() notificationsSelected = notificationsTimeline.getselection() if len(homeSelected) > 0: index = homeSelected[0] toot = self.timelines["home"][index] timeline = self.timelines["home"] elif len(localSelected) > 0: index = localSelected[0] toot = self.timelines["local"][index] timeline = self.timelines["local"] elif len(notificationsSelected) > 0: index = notificationsSelected[0] toot = self.timelines["notifications"][index] timeline = self.timelines["notifications"] else: return None, None, None, None if toot.get("reblog") and resolve_boosts: return toot["reblog"], toot, timeline, index elif toot.get("status"): return toot["status"], toot, timeline, index else: return toot, None, timeline, index def updateTimeline(self, name, limit = None): """ Pulls a timeline from the server and updates the global dicts TODO: hashtags and lists """ params = {} if limit: params["limit"] = limit if len(self.timelines[name]) > 0: params["min_id"] = self.timelines[name][0]["id"] if name == "home": path = "/api/v1/timelines/home" elif name == "local": path = "/api/v1/timelines/public" params["local"] = "true" elif name == "public": # not currently used anywhere path = "/api/v1/timelines/public" elif name == "notifications": path = "/api/v1/notifications" else: dprint("Unknown timeline name: %s" % name) return encoded_params = urllib.urlencode(params) data = handleRequest(self.app, path + "?" + encoded_params, use_token=1) if not data: # handleRequest failed and should have popped an error dialog return # if data is a list, it worked if type(data) == type([]): for i in range(len(data)-1, -1, -1): self.timelines[name].insert(0, data[i]) # if data is a dict, it failed elif type(data) == type({}) and data.get("error") is not None: okDialog("Server error when refreshing %s timeline:\r\r %s" % (name, data['error'])) # i don't think this is reachable, but just in case... else: okDialog("Server error when refreshing %s timeline. Unable to determine data type." % name) \ No newline at end of file diff --git a/TimelineWindow.py b/TimelineWindow.py new file mode 100755 index 0000000..ffd6ba3 --- /dev/null +++ b/TimelineWindow.py @@ -0,0 +1 @@ +""" Macstodon - a Mastodon client for classic Mac OS MIT License Copyright (c) 2022-2023 Scott Small and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # ############## # Python Imports # ############## import Lists import Qd import re import string import urllib import W from EasyDialogs import AskString # ########## # My Imports # ########## from MacstodonConstants import VERSION from MacstodonHelpers import attachmentsDialog, cleanUpUnicode, dprint, handleRequest, \ ImageWidget, LinkExtractor, linksDialog, okDialog, okCancelDialog, TitledEditText, \ TimelineList # ########### # Application # ########### class TimelineWindow(W.Window): def __init__(self): """ Initializes the TimelineWindow class. """ # Set window size. Default to 600x400 which fits nicely in a 640x480 display. # However if you're on a compact Mac that is 512x342, we need to make it smaller. screenbounds = Qd.qd.screenBits.bounds if screenbounds[2] <= 600 and screenbounds[3] <= 400: bounds = (0, 20, 512, 342) else: bounds = (600, 400) self.defaulttext = "Click on a toot or notification in one of the above lists..." self.timelines = { "home": [], "local": [], "notifications": [] } w = W.Window.__init__(self, bounds, "Macstodon %s - Timeline" % VERSION, minsize=(512, 342)) self.setupwidgets() # ######################### # Window Handling Functions # ######################### def setupwidgets(self): """ Defines the Timeline window """ self.panes = W.HorizontalPanes((8, 8, -8, -20), (0.6, 0.4)) self.panes.tlpanes = W.VerticalPanes(None, (0.34, 0.33, 0.33)) self.panes.tlpanes.home = TimelineList( None, "Home Timeline", self.timelines["home"], btnCallback=self.refreshHomeCallback, callback=self.homeClickCallback, flags=Lists.lOnlyOne ) self.panes.tlpanes.local = TimelineList( None, "Local Timeline", self.timelines["local"], btnCallback=self.refreshLocalCallback, callback=self.localClickCallback, flags=Lists.lOnlyOne ) self.panes.tlpanes.notifications = TimelineList( None, "Notifications", self.timelines["notifications"], btnCallback=self.refreshNotificationsCallback, callback=self.notificationClickCallback, flags=Lists.lOnlyOne ) self.panes.tootgroup = W.Group(None) self.panes.tootgroup.toottxt = TitledEditText( (56, 0, -24, -20), title="", text=self.defaulttext, readonly=1, vscroll=1 ) # Links and Attachment buttons self.panes.tootgroup.links = ImageWidget( (-20, 52, 16, 16), pixmap=self.parent.pctLnkDis, callback=self.linksCallback ) self.panes.tootgroup.attch = ImageWidget( (-20, 70, 16, 16), pixmap=self.parent.pctAtcDis, callback=self.attachmentsCallback ) # Avatar, reply/boost/favourite/bookmark buttons self.panes.tootgroup.authorimg = ImageWidget((0, 0, 24, 24), callback=self.avatarClickCallback) self.panes.tootgroup.boosterimg = ImageWidget((24, 24, 24, 24), callback=self.boosterClickCallback) self.panes.tootgroup.reply = ImageWidget( (4, 52, 16, 16), pixmap=self.parent.pctRplDis, callback=self.replyCallback ) self.panes.tootgroup.rpnum = W.TextBox((24, 55, 28, 16), "") self.panes.tootgroup.favrt = ImageWidget( (4, 70, 16, 16), pixmap=self.parent.pctFvtDis, callback=self.favouriteCallback ) self.panes.tootgroup.fvnum = W.TextBox((24, 73, 28, 16), "") self.panes.tootgroup.boost = ImageWidget( (4, 88, 16, 16), pixmap=self.parent.pctBstDis, callback=self.boostCallback ) self.panes.tootgroup.bonum = W.TextBox((24, 91, 28, 16), "") self.panes.tootgroup.bmark = ImageWidget( (4, 106, 16, 16), pixmap=self.parent.pctBkmDis, callback=self.bookmarkCallback ) self.panes.tootgroup.logoutbutton = W.Button((56, -15, 80, 0), "Logout", self.close) self.panes.tootgroup.prefsbutton = W.Button((146, -15, 80, 0), "Preferences", self.prefsCallback) self.panes.tootgroup.profilebutton = W.Button((-170, -15, 80, 0), "Find User", self.profileCallback) self.panes.tootgroup.tootbutton = W.Button((-80, -15, 80, 0), "Post Toot", self.tootCallback) def open(self): """ When the timeline window is opened, populate the timelines. """ app = self.parent prefs = app.getprefs() W.Window.open(self) initial_toots = int(prefs.toots_to_load_startup) if initial_toots: self.refreshHomeCallback(initial_toots) self.refreshLocalCallback(initial_toots) self.refreshNotificationsCallback(initial_toots) def close(self): """ When the timeline window is closed, return to the login window. """ for window in self.parent.profilewindows.values(): window.close() self.parent.loginwindow = self.parent.LoginWindow() self.parent.loginwindow.open() W.Window.close(self) # ####################### # Menu Handling Functions # ####################### def can_logout(self, menuitem): """ Enable the Logout menu item when the Timeline window is open and active. """ return 1 def domenu_logout(self, *args): """ Log out when the Logout menu item is selected. """ self.close() def can_prefs(self, menuitem): """ Enable the Preferences menu item when the Timeline window is open and active. """ return 1 def domenu_prefs(self, *args): """ Open the Preferences window when the Preferences menu item is selected. """ win = self.parent.PrefsWindow() win.open() # ################## # Callback Functions # ################## def profileCallback(self): """ Prompt the user for a username when the Find User button is clicked """ acctname = AskString( "Please enter a user and domain name to view their profile.", "username@example.com" ) if acctname: req_data = { "acct": acctname } path = "/api/v1/accounts/lookup?%s" % urllib.urlencode(req_data) data = handleRequest(self.parent, path) if not data: # handleRequest failed and should have popped an error dialog return if data.get("error_description") is not None: okDialog("Server error when querying user ID:\r\r %s" % data['error_description']) elif data.get("error") is not None: okDialog("Server error when querying user ID:\r\r %s" % data['error']) else: win = self.parent.ProfileWindow(data) win.open() def prefsCallback(self): """ Open the Preferences window when the Preferences button is clicked. """ win = self.parent.PrefsWindow() win.open() def avatarClickCallback(self): """ Run when clicking on a user's avatar. Opens the user's profile. """ dprint("clicked on avatar") toot, _, _, _ = self.getSelectedToot(resolve_boosts=1) if toot: win = self.parent.ProfileWindow(toot["account"]) win.open() else: okDialog("Please select a toot, then click on the author's avatar to view their profile.") def boosterClickCallback(self): dprint("clicked on booster") toot, _, _, _ = self.getSelectedToot(resolve_boosts=0) if toot: win = self.parent.ProfileWindow(toot["account"]) win.open() else: okDialog("Please select a toot, then click on the author's avatar to view their profile.") def timelineClickCallback(self, name): """ Run when the user clicks somewhere in the named timeline """ if name == "home": list = self.panes.tlpanes.home elif name == "local": list = self.panes.tlpanes.local selected = list.getselection() if len(selected) < 1: self.panes.tootgroup.authorimg.clearImage() self.panes.tootgroup.boosterimg.clearImage() self.panes.tootgroup.reply.setImage(self.parent.pctRplDis) self.panes.tootgroup.favrt.setImage(self.parent.pctFvtDis) self.panes.tootgroup.boost.setImage(self.parent.pctBstDis) self.panes.tootgroup.bmark.setImage(self.parent.pctBkmDis) self.panes.tootgroup.links.setImage(self.parent.pctLnkDis) self.panes.tootgroup.attch.setImage(self.parent.pctAtcDis) self.panes.tootgroup.fvnum.set("") self.panes.tootgroup.bonum.set("") self.panes.tootgroup.rpnum.set("") self.panes.tootgroup.toottxt.setTitle("") self.panes.tootgroup.toottxt.set(self.defaulttext) return else: index = selected[0] toot = self.timelines[name][index] self.formatAndDisplayToot(toot) def homeClickCallback(self): """ Run when the user clicks somewhere in the home timeline """ self.panes.tlpanes.local.setselection([-1]) self.panes.tlpanes.notifications.setselection([-1]) self.timelineClickCallback("home") def localClickCallback(self): """ Run when the user clicks somewhere in the local timeline """ self.panes.tlpanes.home.setselection([-1]) self.panes.tlpanes.notifications.setselection([-1]) self.timelineClickCallback("local") def notificationClickCallback(self): """ Run when the user clicks somewhere in the notification timeline """ self.panes.tlpanes.home.setselection([-1]) self.panes.tlpanes.local.setselection([-1]) list = self.panes.tlpanes.notifications selected = list.getselection() if len(selected) < 1: self.panes.tootgroup.authorimg.clearImage() self.panes.tootgroup.boosterimg.clearImage() self.panes.tootgroup.reply.setImage(self.parent.pctRplDis) self.panes.tootgroup.favrt.setImage(self.parent.pctFvtDis) self.panes.tootgroup.boost.setImage(self.parent.pctBstDis) self.panes.tootgroup.bmark.setImage(self.parent.pctBkmDis) self.panes.tootgroup.links.setImage(self.parent.pctLnkDis) self.panes.tootgroup.attch.setImage(self.parent.pctAtcDis) self.panes.tootgroup.fvnum.set("") self.panes.tootgroup.bonum.set("") self.panes.tootgroup.rpnum.set("") self.panes.tootgroup.toottxt.setTitle("") self.panes.tootgroup.toottxt.set(self.defaulttext) return else: index = selected[0] notification = self.timelines["notifications"][index] if notification["type"] in ["favourite", "reblog", "status", "mention", "poll", "update"]: toot = notification["status"] self.formatAndDisplayToot(toot) elif notification["type"] == "admin.report": okDialog("Sorry, displaying the notification type '%s' is not supported yet" % notification["type"]) self.panes.tootgroup.authorimg.clearImage() self.panes.tootgroup.boosterimg.clearImage() self.panes.tootgroup.reply.setImage(self.parent.pctRplDis) self.panes.tootgroup.favrt.setImage(self.parent.pctFvtDis) self.panes.tootgroup.boost.setImage(self.parent.pctBstDis) self.panes.tootgroup.bmark.setImage(self.parent.pctBkmDis) self.panes.tootgroup.links.setImage(self.parent.pctLnkDis) self.panes.tootgroup.attch.setImage(self.parent.pctAtcDis) self.panes.tootgroup.fvnum.set("") self.panes.tootgroup.bonum.set("") self.panes.tootgroup.rpnum.set("") self.panes.tootgroup.toottxt.setTitle("") self.panes.tootgroup.toottxt.set(self.defaulttext) else: win = self.parent.ProfileWindow(notification["account"]) win.open() self.panes.tootgroup.authorimg.clearImage() self.panes.tootgroup.boosterimg.clearImage() self.panes.tootgroup.reply.setImage(self.parent.pctRplDis) self.panes.tootgroup.favrt.setImage(self.parent.pctFvtDis) self.panes.tootgroup.boost.setImage(self.parent.pctBstDis) self.panes.tootgroup.bmark.setImage(self.parent.pctBkmDis) self.panes.tootgroup.links.setImage(self.parent.pctLnkDis) self.panes.tootgroup.attch.setImage(self.parent.pctAtcDis) self.panes.tootgroup.fvnum.set("") self.panes.tootgroup.bonum.set("") self.panes.tootgroup.rpnum.set("") self.panes.tootgroup.toottxt.setTitle("") self.panes.tootgroup.toottxt.set(self.defaulttext) def refreshHomeCallback(self, limit=None): """ Run when the user clicks the Refresh button above the home timeline """ self.updateTimeline("home", limit) self.panes.tlpanes.home.set(self.formatTimelineForList("home")) def refreshLocalCallback(self, limit=None): """ Run when the user clicks the Refresh button above the local timeline """ self.updateTimeline("local", limit) self.panes.tlpanes.local.set(self.formatTimelineForList("local")) def refreshNotificationsCallback(self, limit=None): """ Run when the user clicks the Refresh button above the notifications timeline """ self.updateTimeline("notifications", limit) listitems = self.formatNotificationsForList() self.panes.tlpanes.notifications.set(listitems) def tootCallback(self): """ Run when the user clicks the "Post Toot" button from the timeline window. It opens up the toot window. """ self.parent.tootwindow = self.parent.TootWindow() self.parent.tootwindow.open() def replyCallback(self): """ Run when the user clicks the "Reply" button from the timeline window. It opens up the toot window, passing the currently selected toot as a parameter. """ toot, origToot, timeline, index = self.getSelectedToot(resolve_boosts=1) if toot: self.parent.tootwindow = self.parent.TootWindow(replyTo=toot) self.parent.tootwindow.open() else: okDialog("Please select a toot first.") def boostCallback(self): """ Boosts a toot. Removes the boost if the toot was already boosted. """ toot, origToot, timeline, index = self.getSelectedToot(resolve_boosts=1) if toot: if toot["reblogged"]: # already boosted, undo it action = "unreblog" else: # not bookmarked yet action = "reblog" visibility = toot["visibility"] if visibility == "limited" or visibility == "direct": visibility = "public" req_data = { "visibility": visibility } path = "/api/v1/statuses/%s/%s" % (toot["id"], action) data = handleRequest(self.parent, path, req_data, use_token=1) if not data: # handleRequest failed and should have popped an error dialog return if data.get("error_description") is not None: okDialog("Server error when %sing toot:\r\r %s" % (action, data['error_description'])) elif data.get("error") is not None: okDialog("Server error when %sing toot:\r\r %s" % (action, data['error'])) else: if origToot: if origToot.get("reblog"): dprint("overwriting boosted toot") timeline[index]["reblog"] = data["reblog"] else: dprint("overwriting notification") timeline[index]["status"] = data["reblog"] else: dprint("overwriting normal toot") timeline[index] = data["reblog"] okDialog("Toot %sged successfully!" % action) if action == "reblog": self.panes.tootgroup.boost.setImage(self.parent.pctBstClr) else: self.panes.tootgroup.boost.setImage(self.parent.pctBstBnW) else: okDialog("Please select a toot first.") def favouriteCallback(self): """ Favourites a toot. Removes the favourite if the toot was already favourited. """ toot, origToot, timeline, index = self.getSelectedToot(resolve_boosts=1) if toot: if toot["favourited"]: # already favourited, undo it action = "unfavourite" else: # not favourited yet action = "favourite" path = "/api/v1/statuses/%s/%s" % (toot["id"], action) data = handleRequest(self.parent, path, {}, use_token=1) if not data: # handleRequest failed and should have popped an error dialog return if data.get("error_description") is not None: okDialog("Server error when %sing toot:\r\r %s" % (action, data['error_description'])) elif data.get("error") is not None: okDialog("Server error when %sing toot:\r\r %s" % (action, data['error'])) else: if origToot: if origToot.get("reblog"): dprint("overwriting boosted toot") timeline[index]["reblog"] = data else: dprint("overwriting notification") timeline[index]["status"] = data else: dprint("overwriting normal toot") timeline[index] = data okDialog("Toot %sd successfully!" % action) if action == "favourite": self.panes.tootgroup.favrt.setImage(self.parent.pctFvtClr) else: self.panes.tootgroup.favrt.setImage(self.parent.pctFvtBnW) else: okDialog("Please select a toot first.") def bookmarkCallback(self): """ Bookmarks a toot. Removes the bookmark if the toot was already bookmarked. """ toot, origToot, timeline, index = self.getSelectedToot(resolve_boosts=1) if toot: if toot["bookmarked"]: # already bookmarked, undo it action = "unbookmark" else: # not bookmarked yet action = "bookmark" path = "/api/v1/statuses/%s/%s" % (toot["id"], action) data = handleRequest(self.parent, path, {}, use_token=1) if not data: # handleRequest failed and should have popped an error dialog return if data.get("error_description") is not None: okDialog("Server error when %sing toot:\r\r %s" % (action, data['error_description'])) elif data.get("error") is not None: okDialog("Server error when %sing toot:\r\r %s" % (action, data['error'])) else: if origToot: if origToot.get("reblog"): dprint("overwriting boosted toot") timeline[index]["reblog"] = data else: dprint("overwriting notification") timeline[index]["status"] = data else: dprint("overwriting normal toot") timeline[index] = data okDialog("Toot %sed successfully!" % action) if action == "bookmark": self.panes.tootgroup.favrt.setImage(self.parent.pctFvtClr) else: self.panes.tootgroup.favrt.setImage(self.parent.pctFvtBnW) else: okDialog("Please select a toot first.") def linksCallback(self): """ Displays a dialog containing the links in the toot and allows the user to open them. """ toot, origToot, timeline, index = self.getSelectedToot(resolve_boosts=1) if toot: content = toot["content"] # Replace HTML linebreak tags with actual linebreaks content = cleanUpUnicode(content) # Extract links le = LinkExtractor() le.feed(content) le.close() linksDialog(le) else: okDialog("Please select a toot first.") def attachmentsCallback(self): toot, origToot, timeline, index = self.getSelectedToot(resolve_boosts=1) if toot: attachmentsDialog(toot["media_attachments"]) else: okDialog("Please select a toot first.") # #################### # Formatting Functions # #################### def formatAndDisplayToot(self, toot): """ Formats a toot for display and displays it in the bottom third """ prefs = self.parent.getprefs() # clear existing toot self.panes.tootgroup.authorimg.clearImage() self.panes.tootgroup.boosterimg.clearImage() self.panes.tootgroup.reply.setImage(self.parent.pctRplDis) self.panes.tootgroup.favrt.setImage(self.parent.pctFvtDis) self.panes.tootgroup.boost.setImage(self.parent.pctBstDis) self.panes.tootgroup.bmark.setImage(self.parent.pctBkmDis) self.panes.tootgroup.links.setImage(self.parent.pctLnkDis) self.panes.tootgroup.attch.setImage(self.parent.pctAtcDis) self.panes.tootgroup.fvnum.set("") self.panes.tootgroup.bonum.set("") self.panes.tootgroup.rpnum.set("") self.panes.tootgroup.toottxt.setTitle("") self.panes.tootgroup.toottxt.set("Loading toot...") display_name = toot["account"]["display_name"] or toot["account"]["username"] display_name = cleanUpUnicode(display_name) if toot["reblog"]: self.panes.tootgroup.authorimg.resize(24,24) if prefs.show_avatars: image = self.parent.imagehandler.getImageFromURL(toot["reblog"]["account"]["avatar"], "account") bimage = self.parent.imagehandler.getImageFromURL(toot["account"]["avatar"], "account") else: image = None bimage = None reblog_display_name = toot["reblog"]["account"]["display_name"] or toot["reblog"]["account"]["username"] reblog_display_name = cleanUpUnicode(reblog_display_name) title = "%s boosted %s (@%s)" % (display_name, reblog_display_name, toot["reblog"]["account"]["acct"]) content = toot["reblog"]["content"] sensitive = toot["reblog"]["sensitive"] spoiler_text = toot["reblog"]["spoiler_text"] favourites_count = toot["reblog"]["favourites_count"] reblogs_count = toot["reblog"]["reblogs_count"] replies_count = toot["reblog"]["replies_count"] favourited = toot["reblog"]["favourited"] reblogged = toot["reblog"]["reblogged"] bookmarked = toot["reblog"]["bookmarked"] else: self.panes.tootgroup.authorimg.resize(48,48) if prefs.show_avatars: image = self.parent.imagehandler.getImageFromURL(toot["account"]["avatar"], "account") else: image = None bimage = None title = "%s (@%s)" % (display_name, toot["account"]["acct"]) content = toot["content"] sensitive = toot["sensitive"] spoiler_text = toot["spoiler_text"] favourites_count = toot["favourites_count"] reblogs_count = toot["reblogs_count"] replies_count = toot["replies_count"] favourited = toot["favourited"] reblogged = toot["reblogged"] bookmarked = toot["bookmarked"] # Check for CW if sensitive: cwText = "This toot has a content warning. " \ "Press OK to view or Cancel to not view.\r\r%s" try: okCancelDialog(cwText % spoiler_text) except KeyboardInterrupt: self.panes.tootgroup.toottxt.set(self.defaulttext) return # Replace HTML linebreak tags with actual linebreaks content = cleanUpUnicode(content) content = string.replace(content, "
", "\r") content = string.replace(content, "
", "\r") content = string.replace(content, "
", "\r") content = string.replace(content, "

", "") content = string.replace(content, "

", "\r\r") # Extract links le = LinkExtractor() le.feed(content) le.close() # Strip all other HTML tags content = re.sub('<[^<]+?>', '', content) # Render content into UI if image: self.panes.tootgroup.authorimg.setImage(image) if bimage: self.panes.tootgroup.boosterimg.setImage(bimage) self.panes.tootgroup.reply.setImage(self.parent.pctRplBnW) if favourited: self.panes.tootgroup.favrt.setImage(self.parent.pctFvtClr) else: self.panes.tootgroup.favrt.setImage(self.parent.pctFvtBnW) if reblogged: self.panes.tootgroup.boost.setImage(self.parent.pctBstClr) else: self.panes.tootgroup.boost.setImage(self.parent.pctBstBnW) if bookmarked: self.panes.tootgroup.bmark.setImage(self.parent.pctBkmClr) else: self.panes.tootgroup.bmark.setImage(self.parent.pctBkmBnW) self.panes.tootgroup.links.setImage(self.parent.pctLnkBnW) self.panes.tootgroup.attch.setImage(self.parent.pctAtcBnW) self.panes.tootgroup.toottxt.setTitle(title) self.panes.tootgroup.toottxt.set(content) self.panes.tootgroup.fvnum.set(str(favourites_count)) self.panes.tootgroup.bonum.set(str(reblogs_count)) self.panes.tootgroup.rpnum.set(str(replies_count)) def formatTimelineForList(self, name): """ Formats toots for display in a timeline list """ listitems = [] for toot in self.timelines[name]: if toot["reblog"]: if toot["reblog"]["sensitive"]: content = toot["reblog"]["spoiler_text"] else: content = toot["reblog"]["content"] else: if toot["sensitive"]: content = toot["spoiler_text"] else: content = toot["content"] content = cleanUpUnicode(content) # Replace linebreaks with spaces content = string.replace(content, "
", " ") content = string.replace(content, "
", " ") content = string.replace(content, "
", " ") content = string.replace(content, "

", "") content = string.replace(content, "

", " ") # Strip all other HTML tags content = re.sub('<[^<]+?>', '', content) display_name = toot["account"]["display_name"] or toot["account"]["username"] display_name = cleanUpUnicode(display_name) if toot["reblog"]: reblog_display_name = toot["reblog"]["account"]["display_name"] or toot["reblog"]["account"]["username"] reblog_display_name = cleanUpUnicode(reblog_display_name) listitem = "%s boosted %s\r%s" % (display_name, reblog_display_name, content) else: listitem = "%s\r%s" % (display_name, content) listitems.append(listitem) return listitems def formatNotificationsForList(self): """ Formats notifications for display in a list """ listitems = [] for notification in self.timelines["notifications"]: display_name = notification["account"]["display_name"] or notification["account"]["username"] display_name = cleanUpUnicode(display_name) if notification["type"] == "mention": listitem = "%s mentioned you in their toot" % display_name elif notification["type"] == "status": listitem = "%s posted a toot" % display_name elif notification["type"] == "reblog": listitem = "%s boosted your toot" % display_name elif notification["type"] == "follow": listitem = "%s followed you" % display_name elif notification["type"] == "follow_request": listitem = "%s requested to follow you" % display_name elif notification["type"] == "favourite": listitem = "%s favourited your toot" % display_name elif notification["type"] == "poll": listitem = "%s's poll has ended" % display_name elif notification["type"] == "update": listitem = "%s updated their toot" % display_name elif notification["type"] == "admin.sign_up": listitem = "%s signed up" % display_name elif notification["type"] == "admin.report": listitem = "%s filed a report" % display_name else: # unknown type, ignore it, but print to console if debugging dprint("Unknown notification type: %s" % notification["type"]) listitems.append(listitem) return listitems # ################ # Helper Functions # ################ def getSelectedToot(self, resolve_boosts=0): """ Returns the selected toot, the containing toot (if boost or notification), the timeline to which the toot belongs, and the index of the toot in the timeline. """ homeTimeline = self.panes.tlpanes.home localTimeline = self.panes.tlpanes.local notificationsTimeline = self.panes.tlpanes.notifications homeSelected = homeTimeline.getselection() localSelected = localTimeline.getselection() notificationsSelected = notificationsTimeline.getselection() if len(homeSelected) > 0: index = homeSelected[0] toot = self.timelines["home"][index] timeline = self.timelines["home"] elif len(localSelected) > 0: index = localSelected[0] toot = self.timelines["local"][index] timeline = self.timelines["local"] elif len(notificationsSelected) > 0: index = notificationsSelected[0] toot = self.timelines["notifications"][index] timeline = self.timelines["notifications"] else: return None, None, None, None if toot.get("reblog") and resolve_boosts: return toot["reblog"], toot, timeline, index elif toot.get("status"): return toot["status"], toot, timeline, index else: return toot, None, timeline, index def updateTimeline(self, name, limit = None): """ Pulls a timeline from the server and updates the global dicts TODO: hashtags and lists """ params = {} app = self.parent prefs = app.getprefs() if limit: # If a limit was explicitly set in the call, use that params["limit"] = limit else: # Otherwise, use the refresh limit from the prefs if one was set refresh_toots = int(prefs.toots_to_load_refresh) if refresh_toots: params["limit"] = refresh_toots if len(self.timelines[name]) > 0: params["min_id"] = self.timelines[name][0]["id"] if name == "home": path = "/api/v1/timelines/home" elif name == "local": path = "/api/v1/timelines/public" params["local"] = "true" elif name == "public": # not currently used anywhere path = "/api/v1/timelines/public" elif name == "notifications": path = "/api/v1/notifications" else: dprint("Unknown timeline name: %s" % name) return encoded_params = urllib.urlencode(params) if encoded_params: path = path + "?" + encoded_params data = handleRequest(self.parent, path, use_token=1) if not data: # handleRequest failed and should have popped an error dialog return # if data is a list, it worked if type(data) == type([]): for i in range(len(data)-1, -1, -1): self.timelines[name].insert(0, data[i]) self.timelines[name] = self.timelines[name][:int(prefs.toots_per_timeline)] # if data is a dict, it failed elif type(data) == type({}) and data.get("error") is not None: okDialog("Server error when refreshing %s timeline:\r\r %s" % (name, data['error'])) # i don't think this is reachable, but just in case... else: okDialog("Server error when refreshing %s timeline. Unable to determine data type." % name) \ No newline at end of file diff --git a/TootHandler.py b/TootHandler.py deleted file mode 100644 index 3d39d64..0000000 --- a/TootHandler.py +++ /dev/null @@ -1 +0,0 @@ -""" Macstodon - a Mastodon client for classic Mac OS MIT License Copyright (c) 2022-2023 Scott Small and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # ############## # Python Imports # ############## import re import string import W # ########## # My Imports # ########## from MacstodonConstants import VERSION from MacstodonHelpers import cleanUpUnicode, dprint, handleRequest, okDialog, TitledEditText # ########### # Application # ########### class TootHandler: def __init__(self, app): """ Initializes the TootHandler class. """ self.app = app self.replyToID = None self.visibility = "public" # ######################### # Window Handling Functions # ######################### def getTootWindow(self, replyTo=None): """ Defines the Toot window. """ prefs = self.app.getprefs() if not prefs.max_toot_chars: prefs.max_toot_chars = self.getMaxTootChars() prefs.save() tootwindow = W.Dialog((320, 210), "Macstodon %s - Toot" % VERSION) heading = "Type your toot below (max %s characters):" % prefs.max_toot_chars if replyTo: self.replyToID = replyTo["id"] title = "Replying to %s:" % replyTo["account"]["acct"] text = "@%s " % replyTo["account"]["acct"] content = replyTo["content"] # Replace HTML linebreak tags with actual linebreaks content = cleanUpUnicode(content) content = string.replace(content, "
", "\r") content = string.replace(content, "
", "\r") content = string.replace(content, "
", "\r") content = string.replace(content, "

", "") content = string.replace(content, "

", "\r\r") # Strip all other HTML tags content = re.sub('<[^<]+?>', '', content) tootwindow.reply = TitledEditText((10, 6, -10, -140), title=title, text=content, readonly=1, vscroll=1) tootwindow.toot = TitledEditText((10, 76, -10, -70), title=heading, text=text, vscroll=1) else: self.replyToID = None tootwindow.toot = TitledEditText((10, 6, -10, -70), title=heading, vscroll=1) # Visibility radio buttons visButtons = [] tootwindow.vis_public = W.RadioButton((10, 145, 55, 16), "Public", visButtons, self.visPublicCallback) tootwindow.vis_unlisted = W.RadioButton((75, 145, 65, 16), "Unlisted", visButtons, self.visUnlistedCallback) tootwindow.vis_followers = W.RadioButton((150, 145, 75, 16), "Followers", visButtons, self.visFollowersCallback) tootwindow.vis_mentioned = W.RadioButton((230, 145, 75, 16), "Mentioned", visButtons, self.visMentionedCallback) # If replying to an existing toot, default to that toot's visibility. # Default to public for new toots that are not replies. if replyTo: if replyTo["visibility"] == "unlisted": self.visibility = "unlisted" tootwindow.vis_unlisted.set(1) elif replyTo["visibility"] == "private": self.visibility = "private" tootwindow.vis_followers.set(1) elif replyTo["visibility"] == "direct": self.visibility = "direct" tootwindow.vis_mentioned.set(1) else: self.visibility = "public" tootwindow.vis_public.set(1) else: tootwindow.vis_public.set(1) # Content warning checkbox and text field tootwindow.cw = W.CheckBox((10, -45, 30, 16), "CW", self.cwCallback) tootwindow.cw_text = W.EditText((50, -45, -10, 16)) # If replying to a toot with a CW, apply the CW to the reply. # For new toots, default to the CW off. if replyTo: if replyTo["sensitive"]: tootwindow.cw.set(1) tootwindow.cw_text.set(replyTo["spoiler_text"]) else: tootwindow.cw_text.show(0) else: tootwindow.cw_text.show(0) # Close button tootwindow.close_btn = W.Button((10, -22, 60, 16), "Close", tootwindow.close) # Toot button # This button is intentionally not made a default, so that if you press Return # to make a multi-line toot it won't accidentally send. tootwindow.toot_btn = W.Button((-69, -22, 60, 16), "Toot!", self.tootCallback) return tootwindow # ################## # Callback Functions # ################## def cwCallback(self): """ Called when the CW checkbox is ticked or unticked. Used to show/hide the CW text entry field. """ self.app.tootwindow.cw_text.show(not self.app.tootwindow.cw_text._visible) def visPublicCallback(self): """ Sets visibility to public when the Public radio button is clicked. """ self.visibility = "public" def visUnlistedCallback(self): """ Sets visibility to unlisted when the Unlisted radio button is clicked. """ self.visibility = "unlisted" def visFollowersCallback(self): """ Sets visibility to private when the Followers radio button is clicked. """ self.visibility = "private" def visMentionedCallback(self): """ Sets visibility to direct when the Mentioned radio button is clicked. """ self.visibility = "direct" def tootCallback(self): """ Called when the user presses the Toot button, and posts their toot. """ req_data = { "status": self.app.tootwindow.toot.get(), "visibility": self.visibility } if self.replyToID: req_data["in_reply_to_id"] = self.replyToID if self.app.tootwindow.cw.get() == 1: req_data["sensitive"] = "true" req_data["spoiler_text"] = self.app.tootwindow.cw_text.get() path = "/api/v1/statuses" data = handleRequest(self.app, path, req_data, use_token=1) if not data: # handleRequest failed and should have popped an error dialog return if data.get("error_description") is not None: okDialog("Server error when posting toot:\r\r %s" % data['error_description']) elif data.get("error") is not None: okDialog("Server error when posting toot:\r\r %s" % data['error']) else: okDialog("Tooted successfully!") self.app.tootwindow.close() # ################ # Helper Functions # ################ def getMaxTootChars(self): """ Gets the maximum allowed number of characters in a toot. Not all instances support this, the default is 500 characters if not present in the response. """ path = "/api/v1/instance" data = handleRequest(self.app, path) if not data: return 500 if data.get("error_description") is not None: dprint("Server error when getting max toot chars: %s" % data["error_description"]) return 500 elif data.get("error") is not None: dprint("Server error when getting max toot chars: %s" % data["error"]) return 500 else: max_toot_chars = data.get("max_toot_chars", 500) dprint("max toot chars: %s" % max_toot_chars) return max_toot_chars \ No newline at end of file diff --git a/TootWindow.py b/TootWindow.py new file mode 100755 index 0000000..eed09a9 --- /dev/null +++ b/TootWindow.py @@ -0,0 +1 @@ +""" Macstodon - a Mastodon client for classic Mac OS MIT License Copyright (c) 2022-2023 Scott Small and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # ############## # Python Imports # ############## import re import string import W # ########## # My Imports # ########## from MacstodonConstants import VERSION from MacstodonHelpers import cleanUpUnicode, dprint, handleRequest, okDialog, TitledEditText # ########### # Application # ########### class TootWindow(W.ModalDialog): def __init__(self, replyTo=None, replyUser=None, visibility="public"): """ Initializes the TootWindow class. """ self.replyTo = replyTo self.replyUser = replyUser self.visibility = visibility W.ModalDialog.__init__(self, (320, 210), "Macstodon %s - Toot" % VERSION) self.setupwidgets() # ######################### # Window Handling Functions # ######################### def setupwidgets(self): """ Defines the Toot window. """ # this isn't the best place for this code to go # but the app crashes if i try to run it earlier prefs = self.parent.getprefs() if not prefs.max_toot_chars: prefs.max_toot_chars = self.getMaxTootChars() prefs.save() heading = "Type your toot below (max %s characters):" % prefs.max_toot_chars if self.replyTo: title = "Replying to %s:" % self.replyTo["account"]["acct"] text = "@%s " % self.replyTo["account"]["acct"] content = self.replyTo["content"] # Replace HTML linebreak tags with actual linebreaks content = cleanUpUnicode(content) content = string.replace(content, "
", "\r") content = string.replace(content, "
", "\r") content = string.replace(content, "
", "\r") content = string.replace(content, "

", "") content = string.replace(content, "

", "\r\r") # Strip all other HTML tags content = re.sub('<[^<]+?>', '', content) self.reply = TitledEditText((10, 6, -10, -140), title=title, text=content, readonly=1, vscroll=1) self.toot = TitledEditText((10, 76, -10, -70), title=heading, text=text, vscroll=1) else: if self.replyUser: text = "@%s " % self.replyUser["acct"] else: text = "" self.toot = TitledEditText((10, 6, -10, -70), title=heading, text=text, vscroll=1) # Visibility radio buttons visButtons = [] self.vis_public = W.RadioButton((10, 145, 55, 16), "Public", visButtons, self.visPublicCallback) self.vis_unlisted = W.RadioButton((75, 145, 65, 16), "Unlisted", visButtons, self.visUnlistedCallback) self.vis_followers = W.RadioButton((150, 145, 75, 16), "Followers", visButtons, self.visFollowersCallback) self.vis_mentioned = W.RadioButton((230, 145, 75, 16), "Mentioned", visButtons, self.visMentionedCallback) # If replying to an existing toot, default to that toot's visibility. # Default to public for new toots that are not replies. if self.replyTo: self.visibility = self.replyTo["visibility"] if self.visibility == "unlisted": self.vis_unlisted.set(1) elif self.visibility == "private": self.vis_followers.set(1) elif self.visibility == "direct": self.vis_mentioned.set(1) else: self.vis_public.set(1) # Content warning checkbox and text field self.cw = W.CheckBox((10, -45, 30, 16), "CW", self.cwCallback) self.cw_text = W.EditText((50, -45, -10, 16)) # Close button self.close_btn = W.Button((10, -22, 60, 16), "Close", self.close) # Toot button # This button is intentionally not made a default, so that if you press Return # to make a multi-line toot it won't accidentally send. self.toot_btn = W.Button((-69, -22, 60, 16), "Toot!", self.tootCallback) def open(self): W.Window.open(self) # If replying to a toot with a CW, apply the CW to the reply. # For new toots, default to the CW off. if self.replyTo: if self.replyTo["sensitive"]: self.cw.set(1) if self.replyTo["spoiler_text"][:3] == "re:": self.cw_text.set(self.replyTo["spoiler_text"]) else: self.cw_text.set("re: " + self.replyTo["spoiler_text"]) else: self.cw_text.enable(0) self.cw_text.show(0) else: self.cw_text.enable(0) self.cw_text.show(0) def close(self): W.Window.close(self) # ################## # Callback Functions # ################## def cwCallback(self): """ Called when the CW checkbox is ticked or unticked. Used to show/hide the CW text entry field. """ self.cw_text.select(not self.cw_text._visible) self.cw_text.show(not self.cw_text._visible) self.cw_text.enable(not self.cw_text._enabled) def visPublicCallback(self): """ Sets visibility to public when the Public radio button is clicked. """ self.visibility = "public" def visUnlistedCallback(self): """ Sets visibility to unlisted when the Unlisted radio button is clicked. """ self.visibility = "unlisted" def visFollowersCallback(self): """ Sets visibility to private when the Followers radio button is clicked. """ self.visibility = "private" def visMentionedCallback(self): """ Sets visibility to direct when the Mentioned radio button is clicked. """ self.visibility = "direct" def tootCallback(self): """ Called when the user presses the Toot button, and posts their toot. """ req_data = { "status": self.toot.get(), "visibility": self.visibility } if self.replyTo: req_data["in_reply_to_id"] = self.replyTo["id"] if self.cw.get() == 1: req_data["sensitive"] = "true" req_data["spoiler_text"] = self.cw_text.get() path = "/api/v1/statuses" data = handleRequest(self.parent, path, req_data, use_token=1) if not data: # handleRequest failed and should have popped an error dialog return if data.get("error_description") is not None: okDialog("Server error when posting toot:\r\r %s" % data['error_description']) elif data.get("error") is not None: okDialog("Server error when posting toot:\r\r %s" % data['error']) else: okDialog("Tooted successfully!") self.close() # ################ # Helper Functions # ################ def getMaxTootChars(self): """ Gets the maximum allowed number of characters in a toot. Not all instances support this, the default is 500 characters if not present in the response. """ path = "/api/v1/instance" data = handleRequest(self.parent, path) if not data: dprint("No data returned from server when getting max toot chars") return 500 if data.get("error_description") is not None: dprint("Server error when getting max toot chars: %s" % data["error_description"]) return 500 elif data.get("error") is not None: dprint("Server error when getting max toot chars: %s" % data["error"]) return 500 else: try: # Try the Mastodon 4 official api method first max_toot_chars = data["configuration"]["statuses"]["max_characters"] dprint("max chars: %s" % max_toot_chars) except KeyError: # Fall back to the glitch-soc method max_toot_chars = data.get("max_toot_chars", 500) dprint("max toot chars: %s" % max_toot_chars) return max_toot_chars \ No newline at end of file diff --git a/readme_screenshots/attachments.png b/readme_screenshots/attachments.png new file mode 100644 index 0000000000000000000000000000000000000000..5539c4a474bad118f2d34d859c7c0b9339e0e73c GIT binary patch literal 12645 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 literal 0 HcmV?d00001 diff --git a/readme_screenshots/links.png b/readme_screenshots/links.png new file mode 100644 index 0000000000000000000000000000000000000000..ef2616ef138cbf046ff485e966e3734f4fdeacf8 GIT binary patch literal 11571 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 VEz#f4`15xTV}rZ;6}pa3{tueLEMfov literal 0 HcmV?d00001 diff --git a/readme_screenshots/prefs.png b/readme_screenshots/prefs.png new file mode 100644 index 0000000000000000000000000000000000000000..2b5c5e88a92a9859bf693b83be44648e9619598c GIT binary patch literal 9270 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 literal 0 HcmV?d00001 diff --git a/readme_screenshots/timeline.png b/readme_screenshots/timeline.png index 89cdda7c6ac99569d9e4d2404b9a71fe306e2b1f..cf370216e3736c0f676738e59b1ed70a026c4b30 100644 GIT binary patch literal 43366 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 literal 40594 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�=HPJl{x*Y}xYVvJePx<=6mf&ij8SF&?<@ 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 types.UnicodeType: s = s.decode('utf8') #convert ascii to Unicode #str() should, strictly speaking, not return a Unicode #string, but NavigableText never checks and will return #Unicode data if it was initialised with it. s = s + str(c) return s def isSelfClosing(self): return self.name in BeautifulSoup.SELF_CLOSING_TAGS def append(self, tag): self.contents.append(tag) def first(self, name=None, attrs={}, contents=None, recursive=1): r = None l = self.fetch(name, attrs, contents, recursive) if l: r = l[0] return r def _sqlStyleStringMatch(self, match, matchAgainst): result = (match == matchAgainst) if len(matchAgainst) > 1 and matchAgainst[0] == '%' and matchAgainst[-1] == '%' and matchAgainst[-2] != '\\': result = (match.find(matchAgainst[1:-1]) != -1) elif matchAgainst[0] == '%': findVal = match.rfind(matchAgainst[1:]) result = findVal != -1 and findVal == len(match)-len(matchAgainst)+1 elif matchAgainst[-1] == '%': result = match.find(matchAgainst[:-1]) == 0 return result def fetch(self, name=None, attrs={}, contents=None, recursive=1): """Extracts Tag objects that match the given criteria. You can specify the name of the Tag, any attributes you want the Tag to have, and what text and Tags you want to see inside the Tag.""" if contents and type(contents) != type([]): contents = [contents] results = [] for i in self.contents: if isinstance(i, Tag): if not name or i.name == name: match = 1 for attr, matchAgainst in attrs.items(): match = i.get(attr) #By default, find the specific matchAgainst called for. #Use SQL-style wildcards to find substrings, prefix, #suffix, etc. Or use a regular expression object #to do RE matching. result = (match == matchAgainst) if match and matchAgainst: if type(matchAgainst) == types.StringType: #It's a string, possibly with SQL wildcards. result = self._sqlStyleStringMatch(match, matchAgainst) elif hasattr(matchAgainst, 'match'): #It's a regexp object result = matchAgainst.match(match) else: #Assume it's a predicate callable result = matchAgainst(match) if not result: match = 0 break match = match and (not contents or i.contents == contents) if match: results.append(i) if recursive: results.extend(i.fetch(name, attrs, contents, recursive)) return results class BeautifulSoup(SGMLParser, Tag): """The actual parser. It knows the following facts about HTML, and not much else: * Some tags have no closing tag and should be interpreted as being closed as soon as they are encountered. * Most tags can't be nested; encountering an open tag when there's already an open tag of that type in the stack means that the previous tag of that type should be implicitly closed. However, some tags can be nested. When a nestable tag is encountered, it's okay to close all unclosed tags up to the last nestable tag. It might not be safe to close any more, so that's all it closes. * The text inside some tags (ie. 'script') may contain tags which are not really part of the document and which should be parsed as text, not tags. If you want to parse the text as tags, you can always get it and parse it explicitly.""" SELF_CLOSING_TAGS = ['br', 'hr', 'input', 'img', 'meta', 'spacer', 'link', 'frame'] NESTABLE_TAGS = ['font', 'table', 'tr', 'td', 'th', 'tbody', 'p', 'div', 'li', 'ul', 'ol'] QUOTE_TAGS = ['script'] IMPLICITLY_CLOSE_TAGS = 1 def __init__(self, text=None): Tag.__init__(self, '[document]') SGMLParser.__init__(self) self.quoteStack = [] self.hideTag = 1 self.reset() if text: self.feed(text) def feed(self, text): SGMLParser.feed(self, text) self.endData() def reset(self): SGMLParser.reset(self) self.currentData = '' self.currentTag = None self.tagStack = [] self.pushTag(self) def popTag(self, closedTagName=None): tag = self.tagStack.pop() if closedTagName == tag.name: tag.foundClose = 1 # Tags with just one string-owning child get the same string # property as the child, so that soup.tag.string is shorthand # for soup.tag.contents[0].string if len(self.currentTag.contents) == 1 and \ hasattr(self.currentTag.contents[0], 'string'): self.currentTag.string = self.currentTag.contents[0].string #print "Pop", tag.name self.currentTag = self.tagStack[-1] return self.currentTag def pushTag(self, tag): #print "Push", tag.name if self.currentTag: self.currentTag.append(tag) self.tagStack.append(tag) self.currentTag = self.tagStack[-1] def endData(self): if self.currentData: if not string.strip(self.currentData): if '\n' in self.currentData: self.currentData = '\n' else: self.currentData = ' ' o = NavigableText(self.currentData, self.currentTag, self.previous) if self.previous: self.previous.next = o self.previous = o self.currentTag.contents.append(o) self.currentData = '' def _popToTag(self, name, closedTag=0): """Pops the tag stack up to and including the most recent instance of the given tag. If a list of tags is given, will accept any of those tags as an excuse to stop popping, and will *not* pop the tag that caused it to stop popping.""" if self.IMPLICITLY_CLOSE_TAGS: closedTag = 1 numPops = 0 mostRecentTag = None oneTag = (type(name) == types.StringType) for i in range(len(self.tagStack)-1, 0, -1): thisTag = self.tagStack[i].name if (oneTag and thisTag == name) \ or (not oneTag and thisTag in name): numPops = len(self.tagStack)-i break if not oneTag: numPops = numPops - 1 closedTagName = None if closedTag: closedTagName = name for i in range(0, numPops): mostRecentTag = self.popTag(closedTagName) return mostRecentTag def unknown_starttag(self, name, attrs): if self.quoteStack: #This is not a real tag. #print "<%s> is not real!" % name attrs = map(lambda(x, y): '%s="%s"' % (x, y), attrs) self.handle_data('<%s %s>' % (name, attrs)) return self.endData() tag = Tag(name, attrs, self.currentTag, self.previous) if self.previous: self.previous.next = tag self.previous = tag if not name in self.SELF_CLOSING_TAGS: if name in self.NESTABLE_TAGS: self._popToTag(self.NESTABLE_TAGS) else: self._popToTag(name) self.pushTag(tag) if name in self.SELF_CLOSING_TAGS: self.popTag() if name in self.QUOTE_TAGS: #print "Beginning quote (%s)" % name self.quoteStack.append(name) def unknown_endtag(self, name): if self.quoteStack and self.quoteStack[-1] != name: #This is not a real end tag. #print " is not real!" % name self.handle_data('' % name) return self.endData() self._popToTag(name, 1) if self.quoteStack and self.quoteStack[-1] == name: #print "That's the end of %s!" % self.quoteStack[-1] self.quoteStack.pop() def handle_data(self, data): self.currentData = self.currentData + data def handle_pi(self, text): "Propagate processing instructions right through." self.handle_data("" % text) def handle_comment(self, text): "Propagate comments right through." self.handle_data("" % text) def handle_charref(self, ref): "Propagate char refs right through." self.handle_data('&#%s;' % ref) def handle_entityref(self, ref): "Propagate entity refs right through." self.handle_data('&%s;' % ref) def handle_decl(self, data): "Propagate DOCTYPEs right through." self.handle_data('' % data) class BeautifulStoneSoup(BeautifulSoup): """A version of BeautifulSoup that doesn't know anything at all about what HTML tags have special behavior. Useful for parsing things that aren't HTML, or when BeautifulSoup makes an assumption counter to what you were expecting.""" IMPLICITLY_CLOSE_TAGS = 0 SELF_CLOSING_TAGS = [] NESTABLE_TAGS = [] QUOTE_TAGS = [] if __name__ == '__main__': #Here are some examples. #Generally putting it through its paces. #--------------------------------------- text = ''' The Title Link text (italicized) Link text 2 ''' soup = BeautifulSoup(text) print soup("a") #Returns a list of 2 Tag objects, one for each link in #the source print soup.first("a", {'class':'foo'})['href'] #Returns http://www.crummy.com/ print soup.first("title").contents[0] #Returns "The title" print soup.first("a", {'href':'http://www.crummy.com/'}).first("i").contents[0] #Returns "text (italicized)" #Different ways of matching an attribute #--------------------------------------- import re soup = BeautifulSoup('') print soup.first('img', {'src' : 'foo.jpg'}) print soup.first('img', {'src' : re.compile('.*\.jpg')}) print soup.first('img', {'src' : lambda(x): x in ('foo.jpg', 'foo.png')}) #Example of SQL-style attribute wildcards -- all four 'find' calls will #find the link. These are obsolete and will be removed in the next #version. Use a compiled RE object instead #---------------------------------------------------------------------- soup = BeautifulSoup() soup.feed('''bla''') print soup.fetch('a', {'href': 'http://%'}) print soup.fetch('a', {'href': '%.com/'}) print soup.fetch('a', {'href': '%o.c%'}) #Example with horrible HTML #-------------------------- soup = BeautifulSoup(''' Go here or go Home ''') print soup.fetch('a') #Returns a list of 2 Tag objects. print soup.first(attrs={'href': 'here.html'})['class'] #Returns "that" print soup.first(attrs={'class': 'that'}).first('i').contents[0] #returns "here" \ No newline at end of file