@@ -310,6 +310,106 @@ def get_members(self) -> list[dict]:
310310 api = self ._get_api (page )
311311 return api .get_members ()
312312
313+ def _build_workspace_sync_plan (
314+ self ,
315+ members : list [dict [str , Any ]],
316+ pending_invites : list [dict [str , Any ]],
317+ ) -> dict [str , Any ]:
318+ local_emails = {w .email .strip ().lower () for w in self ._get_workers () if w .email }
319+ admin_email = (self .admin_email or "" ).strip ().lower ()
320+
321+ extra_members : list [dict [str , str ]] = []
322+ extra_invites : list [dict [str , str ]] = []
323+ skipped : list [dict [str , str ]] = []
324+
325+ for member in members :
326+ email = str (member .get ("email" ) or "" ).strip ()
327+ if not email :
328+ continue
329+ email_key = email .lower ()
330+ role = str (member .get ("role" ) or "" )
331+
332+ if email_key == admin_email :
333+ skipped .append ({"type" : "member" , "email" : email , "reason" : "self" })
334+ continue
335+ if role and role != "standard-user" :
336+ skipped .append ({"type" : "member" , "email" : email , "reason" : f"role:{ role } " })
337+ continue
338+ if email_key in local_emails :
339+ continue
340+
341+ extra_members .append (
342+ {
343+ "email" : email ,
344+ "id" : str (member .get ("id" ) or "" ),
345+ "role" : role or "standard-user" ,
346+ }
347+ )
348+
349+ for invite in pending_invites :
350+ email = str (invite .get ("email" ) or invite .get ("email_address" ) or "" ).strip ()
351+ if not email :
352+ continue
353+ email_key = email .lower ()
354+ if email_key == admin_email :
355+ skipped .append ({"type" : "invite" , "email" : email , "reason" : "self" })
356+ continue
357+ if email_key in local_emails :
358+ continue
359+
360+ extra_invites .append ({"email" : email })
361+
362+ return {
363+ "admin_email" : self .admin_email ,
364+ "workspace_id" : self .account_id ,
365+ "local_workers_total" : len (local_emails ),
366+ "members_total" : len (members ),
367+ "pending_invites_total" : len (pending_invites ),
368+ "extra_members" : extra_members ,
369+ "extra_invites" : extra_invites ,
370+ "skipped" : skipped ,
371+ "removed_members" : [],
372+ "removed_invites" : [],
373+ }
374+
375+ def sync_workspace (self , * , dry_run : bool = True ) -> dict [str , Any ]:
376+ page = self ._ensure_admin_page ()
377+ api = self ._get_api (page )
378+
379+ members = api .get_members ()
380+ pending_invites = api .get_pending_invites ()
381+ result = self ._build_workspace_sync_plan (members , pending_invites )
382+ result ["dry_run" ] = dry_run
383+
384+ if dry_run :
385+ self ._log (f"WS preview: лишних участников { len (result ['extra_members' ])} , лишних инвайтов { len (result ['extra_invites' ])} " )
386+ return result
387+
388+ for member in result ["extra_members" ]:
389+ member_id = member .get ("id" )
390+ email = member .get ("email" , "" )
391+ if not member_id :
392+ self ._log (f"[предупреждение] Пропускаю участника без id: { email } " )
393+ continue
394+ api .delete_member (member_id )
395+ result ["removed_members" ].append (email )
396+ self ._log (f"Удалён участник WS: { email } " )
397+
398+ for invite in result ["extra_invites" ]:
399+ email = invite .get ("email" , "" )
400+ if not email :
401+ continue
402+ api .delete_invite (email )
403+ result ["removed_invites" ].append (email )
404+ self ._log (f"Удалён инвайт WS: { email } " )
405+
406+ self ._log (
407+ "WS синхронизирован: "
408+ f"участников удалено { len (result ['removed_members' ])} , "
409+ f"инвайтов удалено { len (result ['removed_invites' ])} "
410+ )
411+ return result
412+
313413 def register_slot (self , worker : WorkerAccount , invite_url : str ) -> None :
314414 """Зарегистрировать приглашённый аккаунт через инвайт-ссылку."""
315415 mailbox = self ._mailboxes .get (worker .email )
0 commit comments