Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 10 additions & 21 deletions inbox/events/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from inbox.models.backends.oauth import token_manager
from inbox.models.calendar import Calendar
from inbox.models.event import Event
from inbox.models.session import session_scope

log = get_logger()

Expand All @@ -19,10 +18,10 @@ class AbstractEventsProvider(abc.ABC):
specified account.
"""

def __init__(self, account_id: int, namespace_id: int):
self.account_id = account_id
self.namespace_id = namespace_id
self.log = log.new(account_id=account_id, component="calendar sync")
def __init__(self, account: Account):
self.account = account
self.namespace_id = account.namespace.id
self.log = log.new(account_id=account.id, component="calendar sync")

# A hash to store whether a calendar is read-only or not.
# This is a bit of a hack because this isn't exposed at the event level
Expand Down Expand Up @@ -54,34 +53,28 @@ def sync_events(
raise NotImplementedError()

@abc.abstractmethod
def webhook_notifications_enabled(self, account: Account) -> bool:
def webhook_notifications_enabled(self) -> bool:
"""
Return True if webhook notifications are enabled for a given account.
"""
raise NotImplementedError()

@abc.abstractmethod
def watch_calendar_list(self, account: Account) -> Optional[datetime.datetime]:
def watch_calendar_list(self) -> Optional[datetime.datetime]:
"""
Subscribe to webhook notifications for changes to calendar list.

Arguments:
account: The account

Returns:
The expiration of the notification channel
"""
raise NotImplementedError()

@abc.abstractmethod
def watch_calendar(
self, account: Account, calendar: Calendar
) -> Optional[datetime.datetime]:
def watch_calendar(self, calendar: Calendar) -> Optional[datetime.datetime]:
"""
Subscribe to webhook notifications for changes to events in a calendar.

Arguments:
account: The account
calendar: The calendar

Returns:
Expand All @@ -102,13 +95,9 @@ def _get_access_token(
Returns:
The token
"""
with session_scope(self.namespace_id) as db_session:
acc = db_session.query(Account).get(self.account_id)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any possible reason why reloading the account information here might've been important? Or is the freshest possible state of the account a) irrelevant, or b) already ensured through different logic somehow?

# This will raise OAuthError if OAuth access was revoked. The
# BaseSyncMonitor loop will catch this, clean up, and exit.
return token_manager.get_token(
acc, force_refresh=force_refresh, scopes=scopes
)
return token_manager.get_token(
self.account, force_refresh=force_refresh, scopes=scopes
)


class CalendarGoneException(Exception):
Expand Down
6 changes: 3 additions & 3 deletions inbox/events/actions/backends/gmail.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@


def remote_create_event(account, event, db_session, extra_args):
provider = GoogleEventsProvider(account.id, account.namespace.id)
provider = GoogleEventsProvider(account)
result = provider.create_remote_event(event, **extra_args)
# The events crud API assigns a random uid to an event when creating it.
# We need to update it to the value returned by the Google calendar API.
Expand All @@ -17,12 +17,12 @@ def remote_create_event(account, event, db_session, extra_args):


def remote_update_event(account, event, db_session, extra_args):
provider = GoogleEventsProvider(account.id, account.namespace.id)
provider = GoogleEventsProvider(account)
provider.update_remote_event(event, **extra_args)


def remote_delete_event(
account, event_uid, calendar_name, calendar_uid, db_session, extra_args
):
provider = GoogleEventsProvider(account.id, account.namespace.id)
provider = GoogleEventsProvider(account)
provider.delete_remote_event(calendar_uid, event_uid, **extra_args)
24 changes: 11 additions & 13 deletions inbox/events/google.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
parse_datetime,
parse_google_time,
)
from inbox.models import Account, Calendar
from inbox.models import Calendar
from inbox.models.backends.oauth import token_manager
from inbox.models.event import EVENT_STATUSES, Event

Expand Down Expand Up @@ -279,15 +279,15 @@ def delete_remote_event(self, calendar_uid, event_uid, **kwargs):

# -------- logic for push notification subscriptions -------- #

def _get_access_token_for_push_notifications(self, account, force_refresh=False):
if not self.webhook_notifications_enabled(account):
def _get_access_token_for_push_notifications(self, force_refresh=False):
if not self.webhook_notifications_enabled():
raise OAuthError("Account not enabled for push notifications.")
return token_manager.get_token(account, force_refresh)
return token_manager.get_token(self.account, force_refresh)

def webhook_notifications_enabled(self, account: Account) -> bool:
return account.get_client_info()[0] in WEBHOOK_ENABLED_CLIENT_IDS
def webhook_notifications_enabled(self) -> bool:
return self.account.get_client_info()[0] in WEBHOOK_ENABLED_CLIENT_IDS

def watch_calendar_list(self, account: Account) -> Optional[datetime.datetime]:
def watch_calendar_list(self) -> Optional[datetime.datetime]:
"""
Subscribe to google push notifications for the calendar list.

Expand All @@ -300,9 +300,9 @@ def watch_calendar_list(self, account: Account) -> Optional[datetime.datetime]:
Returns:
The expiration of the notification channel
"""
token = self._get_access_token_for_push_notifications(account)
token = self._get_access_token_for_push_notifications()
receiving_url = CALENDAR_LIST_WEBHOOK_URL.format(
urllib.parse.quote(account.public_id)
urllib.parse.quote(self.account.public_id)
)

one_week = datetime.timedelta(weeks=1)
Expand Down Expand Up @@ -338,9 +338,7 @@ def watch_calendar_list(self, account: Account) -> Optional[datetime.datetime]:
self._handle_watch_errors(r)
return None

def watch_calendar(
self, account: Account, calendar: Calendar
) -> Optional[datetime.datetime]:
def watch_calendar(self, calendar: Calendar) -> Optional[datetime.datetime]:
"""
Subscribe to google push notifications for a calendar.

Expand All @@ -355,7 +353,7 @@ def watch_calendar(
Returns:
The expiration of the notification channel
"""
token = self._get_access_token_for_push_notifications(account)
token = self._get_access_token_for_push_notifications()
watch_url = WATCH_EVENTS_URL.format(urllib.parse.quote(calendar.uid))
receiving_url = EVENTS_LIST_WEBHOOK_URL.format(
urllib.parse.quote(calendar.public_id)
Expand Down
20 changes: 7 additions & 13 deletions inbox/events/microsoft/events_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@


class MicrosoftEventsProvider(AbstractEventsProvider):
def __init__(self, account_id: int, namespace_id: int):
super().__init__(account_id, namespace_id)
def __init__(self, account: Account):
super().__init__(account)

self.client = MicrosoftGraphClient(
lambda: self._get_access_token(scopes=MICROSOFT_CALENDAR_SCOPES)
Expand Down Expand Up @@ -201,7 +201,7 @@ def _get_event_overrides(

return exceptions, cancellations

def webhook_notifications_enabled(self, account: Account) -> bool:
def webhook_notifications_enabled(self) -> bool:
"""
Return True if webhook notifications are enabled for a given account.

Expand All @@ -225,7 +225,7 @@ def webhook_notifications_enabled(self, account: Account) -> bool:

try:
dummy_subscription = self.client.subscribe_to_calendar_changes(
webhook_url=CALENDAR_LIST_WEBHOOK_URL.format(account.public_id),
webhook_url=CALENDAR_LIST_WEBHOOK_URL.format(self.account.public_id),
secret=config["MICROSOFT_SUBSCRIPTION_SECRET"],
)
except MicrosoftGraphClientException as e:
Expand All @@ -245,33 +245,27 @@ def webhook_notifications_enabled(self, account: Account) -> bool:
self._webhook_notifications_enabled = True
return True

def watch_calendar_list(self, account: Account) -> Optional[datetime.datetime]:
def watch_calendar_list(self) -> Optional[datetime.datetime]:
"""
Subscribe to webhook notifications for changes to calendar list.

Arguments:
account: The account

Returns:
The expiration of the notification channel
"""
response = self.client.subscribe_to_calendar_changes(
webhook_url=CALENDAR_LIST_WEBHOOK_URL.format(account.public_id),
webhook_url=CALENDAR_LIST_WEBHOOK_URL.format(self.account.public_id),
secret=config["MICROSOFT_SUBSCRIPTION_SECRET"],
)

expiration = cast(MsGraphSubscription, response)["expirationDateTime"]

return ciso8601.parse_datetime(expiration).replace(microsecond=0)

def watch_calendar(
self, account: Account, calendar: Calendar
) -> Optional[datetime.datetime]:
def watch_calendar(self, calendar: Calendar) -> Optional[datetime.datetime]:
"""
Subscribe to webhook notifications for changes to events in a calendar.

Arguments:
account: The account
calendar: The calendar

Returns:
Expand Down
52 changes: 23 additions & 29 deletions inbox/events/remote_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,27 +40,26 @@ class EventSync(BaseSyncMonitor):

def __init__(
self,
email_address: str,
provider_name: str,
account_id: int,
namespace_id: int,
account: Account,
provider_class: Type[AbstractEventsProvider],
poll_frequency: int = POLL_FREQUENCY,
):
bind_context(self, "eventsync", account_id)
self.provider = provider_class(account_id, namespace_id)
bind_context(self, "eventsync", account.id)
self.provider = provider_class(account)
self.log = logger.new(
account_id=account_id, component="calendar sync", provider=provider_name
account_id=account.id,
component="calendar sync",
provider=account.verbose_provider,
)

BaseSyncMonitor.__init__(
self,
account_id,
namespace_id,
email_address,
account.id,
account.namespace.id,
account.email_address,
EVENT_SYNC_FOLDER_ID,
EVENT_SYNC_FOLDER_NAME,
provider_name,
account.verbose_provider,
poll_frequency=poll_frequency,
scope="calendar",
)
Comment on lines 55 to 65
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a side note and not something that needs to happen in this PR, but I feel like this number of arguments could warrant using * to force all of them to be passed as kwargs. That way at least it's a bit clearer what the role/expectation is for each variable. For example, I think provider_name=account.verbose_provider is clearer than just passing account.verbose_provider into this constructor.

Expand Down Expand Up @@ -239,19 +238,16 @@ def handle_event_updates(


class WebhookEventSync(EventSync):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
with session_scope(self.namespace_id) as db_session:
account = db_session.query(Account).get(self.account_id)
if (
self.provider.webhook_notifications_enabled(account)
and kwargs.get("poll_frequency") is None
):
# Run the sync loop more frequently if push notifications are
# enabled. Note that we'll only update the calendar if a
# Webhook was receicved recently, or if we haven't synced for
# too long.
self.poll_frequency = PUSH_NOTIFICATION_POLL_FREQUENCY
def __init__(
self, account: Account, provider_class: Type[AbstractEventsProvider],
):
super().__init__(account, provider_class)
if self.provider.webhook_notifications_enabled():
# Run the sync loop more frequently if push notifications are
# enabled. Note that we'll only update the calendar if a
# Webhook was receicved recently, or if we haven't synced for
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: received

# too long.
self.poll_frequency = PUSH_NOTIFICATION_POLL_FREQUENCY

def sync(self) -> None:
"""Query a remote provider for updates and persist them to the
Expand Down Expand Up @@ -295,12 +291,12 @@ def _refresh_webhook_subscriptions(self) -> None:
with session_scope(self.namespace_id) as db_session:
account = db_session.query(Account).get(self.account_id)

if not self.provider.webhook_notifications_enabled(account):
if not self.provider.webhook_notifications_enabled():
self.log.warning("Webhook notifications disabled")
return

if account.needs_new_calendar_list_watch():
calendar_list_expiration = self.provider.watch_calendar_list(account)
calendar_list_expiration = self.provider.watch_calendar_list()
if calendar_list_expiration is not None:
account.new_calendar_list_watch(calendar_list_expiration)

Expand All @@ -311,9 +307,7 @@ def _refresh_webhook_subscriptions(self) -> None:
)
for calendar in calendars_to_watch:
try:
event_list_expiration = self.provider.watch_calendar(
account, calendar
)
event_list_expiration = self.provider.watch_calendar(calendar)
if event_list_expiration is not None:
calendar.new_event_watch(event_list_expiration)
except CalendarGoneException:
Expand Down
18 changes: 3 additions & 15 deletions inbox/mailsync/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,27 +310,15 @@ def start_sync(self, account_id):
if info.get("events", None) and acc.sync_events:
if USE_GOOGLE_PUSH_NOTIFICATIONS and acc.provider == "gmail":
event_sync = WebhookEventSync(
acc.email_address,
acc.verbose_provider,
acc.id,
acc.namespace.id,
provider_class=GoogleEventsProvider,
acc, provider_class=GoogleEventsProvider,
)
elif acc.provider == "gmail":
event_sync = EventSync(
acc.email_address,
acc.verbose_provider,
acc.id,
acc.namespace.id,
provider_class=GoogleEventsProvider,
acc, provider_class=GoogleEventsProvider,
)
elif acc.provider == "microsoft":
event_sync = WebhookEventSync(
acc.email_address,
acc.verbose_provider,
acc.id,
acc.namespace.id,
provider_class=MicrosoftEventsProvider,
acc, provider_class=MicrosoftEventsProvider,
)
self.event_sync_monitors[acc.id] = event_sync
event_sync.start()
Expand Down
Loading