Skip to content

Commit

Permalink
Add required_attendee
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidMikeSimon committed Mar 3, 2024
1 parent adc9e5f commit b8475e4
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 35 deletions.
1 change: 1 addition & 0 deletions vdirsyncer/cli/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ def error_callback(e):
error_callback=error_callback,
partial_sync=pair.partial_sync,
remove_details=pair.remove_details,
required_attendee=pair.required_attendee,
)

if sync_failed:
Expand Down
17 changes: 13 additions & 4 deletions vdirsyncer/sync/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def __init__(self, storage: Storage, status: SubStatus):
self.status = status
self._item_cache = {} # type: ignore[var-annotated]

async def prepare_new_status(self, remove_details: bool = False) -> bool:
async def prepare_new_status(self, remove_details: bool = False, required_attendee: str | None = None) -> bool:
storage_nonempty = False
prefetch = []

Expand All @@ -67,6 +67,8 @@ def _store_props(ident: str, props: ItemMetadata) -> None:
# Prefetch items
if prefetch:
async for href, item, etag in self.storage.get_multi(prefetch):
if required_attendee and not item.has_confirmed_attendee(required_attendee):
continue
if remove_details:
item = item.without_details()
_store_props(
Expand Down Expand Up @@ -107,7 +109,8 @@ async def sync(
force_delete=False,
error_callback=None,
partial_sync="revert",
remove_details: bool=False,
remove_details: bool = False,
required_attendee: str | None = None,
) -> None:
"""Synchronizes two storages.
Expand Down Expand Up @@ -149,8 +152,14 @@ async def sync(
a_info = _StorageInfo(storage_a, SubStatus(status, "a"))
b_info = _StorageInfo(storage_b, SubStatus(status, "b"))

a_nonempty = await a_info.prepare_new_status(remove_details=remove_details)
b_nonempty = await b_info.prepare_new_status(remove_details=remove_details)
a_nonempty = await a_info.prepare_new_status(
remove_details=remove_details,
required_attendee=required_attendee
)
b_nonempty = await b_info.prepare_new_status(
remove_details=remove_details,
required_attendee=required_attendee
)

if status_nonempty and not force_delete:
if a_nonempty and not b_nonempty:
Expand Down
80 changes: 49 additions & 31 deletions vdirsyncer/vobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,20 @@ def with_uid(self, new_uid):
component["UID"] = new_uid

return Item("\r\n".join(parsed.dump_lines()))

def has_confirmed_attendee(self, email: str) -> bool:
"""Returns True if the given attendee has accepted an invite to this event"""
parsed = _Component.parse(self.raw)
stack = [parsed]
while stack:
component = stack.pop()
for attendee_line in component.get_all("ATTENDEE"):
sections = attendee_line.split(";")
if f"CN={email}" in sections and "PARTSTAT=ACCEPTED" in sections:
return True
stack.extend(component.subcomponents)

return False

def without_details(self):
"""Returns a minimal version of this item.
Expand All @@ -78,8 +92,7 @@ def without_details(self):
if subcomp.name != "VTIMEZONE"
]
for field in ["DESCRIPTION", "ORGANIZER", "ATTENDEE", "LOCATION"]:
# Repeatedly delete because some fields can appear multiple times
while field in component:
if field in component:
del component[field]

stack.extend(component.subcomponents)
Expand Down Expand Up @@ -264,6 +277,17 @@ def _get_item_type(components, wrappers):
raise ValueError("Not sure how to join components.")


def _extract_prop_value(line, key):
if line.startswith(key):
prefix_without_params = f"{key}:"
prefix_with_params = f"{key};"
if line.startswith(prefix_without_params):
return line[len(prefix_without_params) :]
elif line.startswith(prefix_with_params):
return line[len(prefix_with_params) :].split(":", 1)[-1]

return None

class _Component:
"""
Raw outline of the components.
Expand Down Expand Up @@ -347,20 +371,15 @@ def dump_lines(self):
def __delitem__(self, key):
prefix = (f"{key}:", f"{key};")
new_lines = []
lineiter = iter(self.props)
while True:
for line in lineiter:
in_prop = False
for line in iter(self.props):
if not in_prop:
if line.startswith(prefix):
break
in_prop = True
else:
new_lines.append(line)
else:
break

for line in lineiter:
if not line.startswith((" ", "\t")):
new_lines.append(line)
break
elif not line.startswith((" ", "\t")):
in_prop = False

self.props = new_lines

Expand All @@ -382,26 +401,25 @@ def __contains__(self, obj):
raise ValueError(obj)

def __getitem__(self, key):
prefix_without_params = f"{key}:"
prefix_with_params = f"{key};"
iterlines = iter(self.props)
for line in iterlines:
if line.startswith(prefix_without_params):
rv = line[len(prefix_without_params) :]
break
elif line.startswith(prefix_with_params):
rv = line[len(prefix_with_params) :].split(":", 1)[-1]
break
else:
try:
return next(self.get_all(key))
except StopIteration:
raise KeyError

for line in iterlines:
if line.startswith((" ", "\t")):
rv += line[1:]

def get_all(self, key: str):
rv = None
for line in iter(self.props):
if rv is None:
rv = _extract_prop_value(line, key)
else:
break

return rv
if line.startswith((" ", "\t")):
rv += line[1:]
else:
yield rv
rv = _extract_prop_value(line, key)

if rv is not None:
yield rv

def get(self, key, default=None):
try:
Expand Down

0 comments on commit b8475e4

Please sign in to comment.