From 06b42b5d73a45b1e0e69bc47931ca874e59dd35d Mon Sep 17 00:00:00 2001 From: Stefano Tranquillini Date: Wed, 17 Jan 2024 12:19:11 +0100 Subject: [PATCH 1/5] open all links in description --- src/requirements.txt | 3 ++- src/vince.py | 27 +++++++++++++++++---------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/requirements.txt b/src/requirements.txt index 696cbb0..5f721db 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -9,4 +9,5 @@ py2app[setuptools] requests chardet appdirs -requests \ No newline at end of file +requests +bs4 \ No newline at end of file diff --git a/src/vince.py b/src/vince.py index 026c049..e748839 100644 --- a/src/vince.py +++ b/src/vince.py @@ -16,6 +16,7 @@ from googleapiclient.discovery import build from googleapiclient.errors import HttpError from appdirs import user_data_dir +from bs4 import BeautifulSoup from datetime import datetime, date, timedelta @@ -122,23 +123,28 @@ def load_events(self): if add_event: event_url = event.get( 'hangoutLink', '') + description = event.get("description","") + urls = self.extract_urls(description) if not event_url: - description = event.get("description","") - urls = self.extract_urls(description) if urls: event_url = urls[0] - d_event = dict(id=id, start=start, end=end, summary=event["summary"], url=event_url, eventType=event['eventType'],visibility=event.get('visibility','default')) + d_event = dict(id=id, start=start, end=end, summary=event["summary"], url=event_url, urls=urls, eventType=event['eventType'],visibility=event.get('visibility','default')) d_events.append(d_event) self.menu_items = d_events except HttpError as err: print(err) - + def extract_urls(self, text): - # Regular expression pattern to match URLs - url_pattern = r"(https?://\S+|meet\.\S+)" + # # Regular expression pattern to match URLs + # url_pattern = r"(https?://\S+|meet\.\S+)" - # Find all occurrences of the pattern in the text - urls = re.findall(url_pattern, text) + # # Find all occurrences of the pattern in the text + # urls = re.findall(url_pattern, text) + soup = BeautifulSoup(text, 'html.parser') + + urls = [] + for link in soup.find_all('a'): + urls.append(link.get('href')) return urls @@ -373,8 +379,9 @@ def send_and_open_link(self, _): sound=True ) if self.settings['link_opening_enabled']: - if event['url']: - webbrowser.open(event['url']) + if event['urls']: + for url in event['urls']: + webbrowser.open(url) @rumps.clicked("Quit") def quit(self, _): From cc4119bd6772948a048929d974f78b1b2baf01e0 Mon Sep 17 00:00:00 2001 From: Stefano Tranquillini Date: Wed, 21 Feb 2024 16:16:25 +0100 Subject: [PATCH 2/5] enabling multiple link opening --- src/setup.py | 2 +- src/vince.py | 194 ++++++++++++++++++++++++++++----------------------- 2 files changed, 109 insertions(+), 87 deletions(-) diff --git a/src/setup.py b/src/setup.py index 109bf71..db646fe 100644 --- a/src/setup.py +++ b/src/setup.py @@ -11,7 +11,7 @@ DATA_FILES = [('', ['credentials.json','icon.png','menu-icon.png'])] OPTIONS = { - 'argv_emulation': True, + 'argv_emulation': False, 'plist': {'LSUIElement': True, 'CFBundleName': 'Vince', 'CFBundleShortVersionString': '0.0.1', diff --git a/src/vince.py b/src/vince.py index e748839..bc4d4ca 100644 --- a/src/vince.py +++ b/src/vince.py @@ -1,3 +1,4 @@ +from datetime import datetime, date, timedelta import rumps import os import os.path @@ -17,9 +18,9 @@ from googleapiclient.errors import HttpError from appdirs import user_data_dir from bs4 import BeautifulSoup - - -from datetime import datetime, date, timedelta +import logging +logging.basicConfig(level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s') def str_truncate(string, width): @@ -70,16 +71,15 @@ def __init__(self): def timely_load_events(self, _): self.load_events() - def load_events(self): # d_events=[] # now = datetime.now(pytz.utc) # i=1 # d_event = dict(id=1, start=now+timedelta(seconds=15), end=now+timedelta(seconds=30), summary=f"Event {i}", url="URL {i}", eventType='',visibility='default') - # d_events.append(d_event) + # d_events.append(d_event) # i=2 # d_event = dict(id=1, start=now+timedelta(seconds=65), end=now+timedelta(seconds=185+60), summary=f"Event {i}", url="http://{i}.com", eventType='',visibility='default') - # d_events.append(d_event) + # d_events.append(d_event) # self.menu_items = d_events # gets all todays' event from calendar try: @@ -87,65 +87,80 @@ def load_events(self): # Get today's date and format it today = datetime.combine(date.today(), datetime.min.time()) # Retrieve events for today - events_result = service.events().list( - calendarId='primary', - timeMin=today.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), - timeMax=(datetime.combine(date.today(), datetime.min.time( - )) + timedelta(days=1)).strftime('%Y-%m-%dT%H:%M:%S.%fZ'), - singleEvents=True, - orderBy='startTime', - showDeleted=False, - ).execute() - - events = events_result.get('items', []) + d_events = [] - if events: - for event in events: - try: - id = event['id'] - start = event['start'].get( - 'dateTime', event['start'].get('date')) - end = event['end'].get( - 'dateTime', event['start'].get('date')) - start = datetime.strptime(start, "%Y-%m-%dT%H:%M:%S%z") - end = datetime.strptime(end, "%Y-%m-%dT%H:%M:%S%z") - except: - # most probably a daily event - continue - add_event = True - # skip declined events. - - if attendees := event.get('attendees', []): - for attendee in attendees: - if attendee.get('self', False): - if attendee['responseStatus'] == 'declined': - add_event = False - if add_event: - event_url = event.get( - 'hangoutLink', '') - description = event.get("description","") - urls = self.extract_urls(description) - if not event_url: - if urls: - event_url = urls[0] - d_event = dict(id=id, start=start, end=end, summary=event["summary"], url=event_url, urls=urls, eventType=event['eventType'],visibility=event.get('visibility','default')) - d_events.append(d_event) + for calendar in self.settings.get('calendars', ['primary']): + try: + events_result = service.events().list( + calendarId=calendar, + timeMin=today.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + timeMax=(datetime.combine(date.today(), datetime.min.time( + )) + timedelta(days=1)).strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + singleEvents=True, + orderBy='startTime', + showDeleted=False, + ).execute() + + events = events_result.get('items', []) + except Exception as e: + print(e) + events = None + if events: + for event in events: + try: + id = event['id'] + start = event['start'].get( + 'dateTime', event['start'].get('date')) + end = event['end'].get( + 'dateTime', event['start'].get('date')) + start = datetime.strptime( + start, "%Y-%m-%dT%H:%M:%S%z") + end = datetime.strptime(end, "%Y-%m-%dT%H:%M:%S%z") + except: + # most probably a daily event + continue + add_event = True + # skip declined events. + + if attendees := event.get('attendees', []): + for attendee in attendees: + if attendee.get('self', False): + if attendee['responseStatus'] == 'declined': + add_event = False + if add_event: + event_url = event.get( + 'hangoutLink', '') + description = event.get("description", "") + urls = self.extract_urls(description) + if not event_url: + if urls: + event_url = urls[0] + else: + urls.append(event_url) + logging.debug( + f"{event['summary']} | {event.get('description','')} | {event_url}") + d_event = dict(id=id, start=start, end=end, summary=event["summary"], url=event_url, + urls=urls, eventType=event['eventType'], visibility=event.get('visibility', 'default')) + d_events.append(d_event) + d_events = sorted(d_events, key=lambda d: d['start']) self.menu_items = d_events except HttpError as err: print(err) - + def extract_urls(self, text): # # Regular expression pattern to match URLs # url_pattern = r"(https?://\S+|meet\.\S+)" - + # # Find all occurrences of the pattern in the text # urls = re.findall(url_pattern, text) soup = BeautifulSoup(text, 'html.parser') - + urls = [] for link in soup.find_all('a'): urls.append(link.get('href')) - + if not urls: + for link in re.findall("(?Phttps?://[^\s]+)", text): + urls.append(link) return urls def build_menu(self): @@ -169,7 +184,7 @@ def build_menu(self): if item['url']: # if there's a meet link it adds the link and the "clicking option" # otherwise the item cannot be clicked. and it look disable. - menu_item.url = item['url'] + menu_item.urls = item['urls'] menu_item.set_callback(self.open_browser) self.menu.add(menu_item) # add the quit button @@ -185,7 +200,8 @@ def build_menu(self): def open_browser(self, sender): if self.settings['link_opening_enabled']: - webbrowser.open(sender.url) + for url in sender.urls: + webbrowser.open(url) @rumps.clicked("Refresh Menu") def refresh_menu(self, _): @@ -209,10 +225,11 @@ def _time_left(self, event_time, current_datetime, show_seconds=False): time_left = event_time - current_datetime time_left_str = str(time_left).split(".")[0] - time_left_str = time_left_str.split(",")[0] # Remove microseconds if present + time_left_str = time_left_str.split( + ",")[0] # Remove microseconds if present hours, remainder = divmod(time_left.seconds, 3600) minutes, seconds = divmod(remainder, 60) - time_left_str = f"{hours:02d}:{minutes:02d}" # :{seconds:02d} + time_left_str = f"{hours:02d}:{minutes:02d}" # :{seconds:02d} if not show_seconds: minutes += 1 if minutes == 60: @@ -257,7 +274,7 @@ def _get_next_events(self): @rumps.timer(1) def update_bar_str(self, _): if self.settings['show_menu_bar']: - # updates the bar + # updates the bar if self.menu_items: current_datetime = datetime.now(pytz.utc) current_events = self._get_current_events() @@ -269,7 +286,10 @@ def update_bar_str(self, _): for event in current_events: hours, minutes, seconds = self._time_left( event['end'], current_datetime, True) - title += f" {str_truncate(event['summary'],20)}: {hours:02d}:{minutes:02d}:{seconds:02d} left" + if hours > 0 or minutes > 15: + title += f" {str_truncate(event['summary'],20)}: {hours:02d}:{minutes:02d}" + else: + title += f" {str_truncate(event['summary'],20)}: {hours:02d}:{minutes:02d}:{seconds:02d}" i_current_events += 1 # separated with comma if more than one if i_current_events < len_current_events: @@ -278,7 +298,8 @@ def update_bar_str(self, _): if not current_events: self.slack_meeting(None) else: - event = sorted(current_events, key=lambda x: x["end"])[0] + event = sorted( + current_events, key=lambda x: x["end"])[0] self.slack_meeting(event) self.current_events = current_events @@ -302,7 +323,6 @@ def update_bar_str(self, _): self.title = f"" else: self.title = f"" - def _str_event_menu_current(self, element): # create the items in the menu. util function @@ -332,27 +352,27 @@ def _str_event_menu_next(self, element): @rumps.timer(1) def send_notification_(self, _): if self.settings['notifications']: - + if self.menu_items: current_datetime = datetime.now(pytz.utc) current_events = self._get_current_events() for event in current_events: hours, minutes, seconds = self._time_left( - event['end'], current_datetime, True) + event['end'], current_datetime, True) # send a notification 5 min before the end that event it's almost over notifications = self.settings['notifications'] for notification in notifications: minute_notification = notification['time_left'] - if hours == 0 and minutes == minute_notification and seconds==0: - rumps.notification( - title=f"{minute_notification} minutes left", - subtitle=f"Just {minute_notification}", - message=f"I said {minute_notification} mins left", - sound=notification['sound'] - ) - + if hours == 0 and minutes == minute_notification and seconds == 0: + rumps.notification( + title=f"{minute_notification} minutes left", + subtitle=f"Just {minute_notification}", + message=f"I said {minute_notification} mins left", + sound=notification['sound'] + ) + # and when it's over - if hours == 0 and minutes == 0 and seconds==0: + if hours == 0 and minutes == 0 and seconds == 0: rumps.notification( title=f"{event['summary']}", subtitle="It's over", @@ -370,7 +390,7 @@ def send_and_open_link(self, _): next_events = self._get_next_events() for event in next_events: hours, minutes, seconds = self._time_left( - event['start'], current_datetime, True) + event['start'], current_datetime, True) if hours == 0 and minutes == 1 and seconds == 0: rumps.notification( title="It's meeting time", @@ -394,8 +414,11 @@ def _convert_minutes_to_epoch(self, mins): return epoch def slack_meeting(self, event, reset=False): + slack_token = self.settings.get('slack_oauth_token',"") + if not slack_token: + return auth = { - 'Authorization': 'Bearer %s' % self.settings['slack_oauth_token']} + 'Authorization': 'Bearer %s' % slack_token} # if reset: # data = {"num_minutes": 0} # res = requests.get('https://slack.com/api/dnd.setSnooze', params=data, @@ -426,8 +449,8 @@ def slack_meeting(self, event, reset=False): status_emoji = ":chef-brb:" else: status_emoji = ":date:" - - if event['visibility'] in ['default','public']: + + if event['visibility'] in ['default', 'public']: status_text = f"{event['summary']} [{event['start'].strftime('%H:%M')}-{event['end'].strftime('%H:%M')}]" else: status_text = f"Meeting [{event['start'].strftime('%H:%M')}-{event['end'].strftime('%H:%M')}]" @@ -449,21 +472,20 @@ def load_settings(self): data_dir = user_data_dir(self.app_name) settings_path = os.path.join(data_dir, "settings.json") default_settings = { + "calendars": ["primary"], "link_opening_enabled": True, "show_menu_bar": True, "notifications": [ { - "time_left":5, - "sound": False - },{ - "time_left":3, - "sound": False - },{ - "time_left":1, - "sound": False - }], - "slack_status_enabled": False, - "slack_oauth_token": "", + "time_left": 5, + "sound": False + }, { + "time_left": 3, + "sound": False + }, { + "time_left": 1, + "sound": False + }], } try: with open(settings_path, "r") as settings_file: From 724587208fb43bc07085960aee5d838c6dea03f4 Mon Sep 17 00:00:00 2001 From: Stefano Tranquillini Date: Thu, 21 Mar 2024 09:09:45 +0100 Subject: [PATCH 3/5] changing changelog before creating official version --- README.md | 11 +---------- src/vince.py | 6 ++++++ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f400f1f..878796b 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,7 @@ # Vince Vince - Early is on time, on time is late and late is unacceptable! -## HOW TO USE: APP NOT VERIFIED -You are required to signup with google to fetch your calandar data. -[**The app is not verified**](https://support.google.com/cloud/answer/7454865?hl=en#:~:text=An%20unverified%20app%20is%20an,their%20data%20from%20deceptive%20apps.) since it's a slow and tedius process that I'm working on that in my free time. - -To use it: - -- When the warning page is shown -- click on advance (bottom left) -- And from there you should be able to use it even if not verified. - +www.stefanotranquillini.com/vince ## Main features ![img](https://github.com/esseti/vince/assets/1928354/a78b9221-67de-4e5a-a13d-e0a874ec5237) diff --git a/src/vince.py b/src/vince.py index bc4d4ca..7d6d32e 100644 --- a/src/vince.py +++ b/src/vince.py @@ -208,6 +208,9 @@ def refresh_menu(self, _): self.load_events() self.build_menu() self.update_exiting_events(None) + + + @rumps.timer(61) def update_exiting_events(self, _): @@ -406,6 +409,9 @@ def send_and_open_link(self, _): @rumps.clicked("Quit") def quit(self, _): print('over') + data_dir = user_data_dir(self.app_name) + file_path = os.path.join(data_dir, "token.json") + os.remove(file_path) rumps.quit_application() def _convert_minutes_to_epoch(self, mins): From 02a0165777e6a22b104012d0b67858d0fa9a2398 Mon Sep 17 00:00:00 2001 From: Stefano Tranquillini Date: Thu, 21 Mar 2024 09:11:06 +0100 Subject: [PATCH 4/5] 1.0.0 --- src/changelog.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/changelog.md b/src/changelog.md index 5ae7417..49e9470 100644 --- a/src/changelog.md +++ b/src/changelog.md @@ -1,4 +1,6 @@ -# next +# [1.0.0] - 2024-03-21 +- STEFANO: a lot of things happened and changed. Since approved by google, it's time to make a major. + # [0.0.2] - 2023-07-31 - STEFANO: a lot of improvments among the rest: - slack integration: now updates the slack status From a01c5878023fb3f5d5af5a0e1b39d7f5fa203fd2 Mon Sep 17 00:00:00 2001 From: Stefano Tranquillini Date: Thu, 21 Mar 2024 09:17:36 +0100 Subject: [PATCH 5/5] fix regexp --- src/vince.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vince.py b/src/vince.py index 7d6d32e..53f6150 100644 --- a/src/vince.py +++ b/src/vince.py @@ -159,7 +159,7 @@ def extract_urls(self, text): for link in soup.find_all('a'): urls.append(link.get('href')) if not urls: - for link in re.findall("(?Phttps?://[^\s]+)", text): + for link in re.findall(r"(?Phttps?://[^\s]+)", text): urls.append(link) return urls