#!/usr/bin/env python3 import copy from common import doors, doorSpecificSchedules, memberLevels, membershipworks from lib.hid.Credential import Credential from lib.hid.DoorController import ROOT, E class Member: def __init__( self, forename="", surname="", membershipWorksID="", middleName="", email="", phone="", cardholderID=None, doorAccess=[], credentials=set(), levels=[], extraLevels=[], schedules=[], ): self.forename = forename self.surname = surname self.membershipWorksID = membershipWorksID self.middleName = middleName self.email = email self.phone = phone self.cardholderID = cardholderID self.doorAccess = doorAccess self.credentials = credentials self.levels = levels self.schedules = schedules def __str__(self): return f"""Name: {self.forename} | {self.middleName} | {self.surname} MembershipWorks ID: {self.membershipWorksID} Email: {self.email} Phone: {self.phone} Cardholder ID: {self.cardholderID} doorAccess: {self.doorAccess} Credentials: {self.credentials} Levels: {self.levels} Schedules: {self.schedules} """ class MembershipworksMember(Member): def __init__(self, data, formerMember=False): super().__init__( data["First Name"], data["Last Name"], membershipWorksID=data["Account ID"], email=data["Email"], phone=data["Phone"], ) if data["Access Card Number"] != "": self.credentials = set( [ Credential( code=( data["Access Card Facility Code"], data["Access Card Number"], ) ) ] ) else: self.credentials = set() self.onHold = data["Account on Hold"] != "" self.limitedOperations = ( data["Access Permitted During Limited Operations"] == "Y" ) self.formerMember = formerMember levels = {k: v for k, v in memberLevels.items() if data[k] == k} self.levels = list(levels.keys()) self.schedules = list(levels.values()) self.extraLevels = { schedule: sum( (doors for prop, doors in props.items() if data[prop] == "Y"), [] ) for schedule, props in doorSpecificSchedules.items() } self.doorAccess = [ door for door, doorData in doors.items() if data["Access " + doorData.access + "?"] == "Y" ] def to_DoorMember(self, door): doorLevels = [k for k, v in self.extraLevels.items() if door.name in v] if ( door.name not in self.doorAccess or self.onHold or self.formerMember or not self.limitedOperations ): schedules = [] else: schedules = self.schedules + doorLevels dm = DoorMember( door, forename=self.forename, surname=self.surname, membershipWorksID=self.membershipWorksID, email=self.email, phone=self.phone, levels=self.levels + doorLevels, doorAccess=self.doorAccess, credentials=self.credentials, schedules=schedules, ) return dm def __str__(self): return ( super().__str__() + f"""OnHold? {self.onHold} Limited Operations Access? {self.limitedOperations} Former Member? {self.formerMember} """ ) class DoorMember(Member): def __init__(self, door, *args, **kwargs): super().__init__(*args, **kwargs) self.door = door @classmethod def from_cardholder(cls, data, door): ch = cls( door=door, forename=data.get("forename", ""), surname=data.get("surname", ""), membershipWorksID=data.attrib.get("custom2", ""), middleName=data.attrib.get("middleName", ""), email=data.attrib.get("email", ""), phone=data.attrib.get("phone", ""), cardholderID=data.attrib["cardholderID"], ) ch.credentials = set( Credential(hex=(c.attrib["rawCardNumber"])) for c in data.findall("{*}Credential") ) ch.levels = data.attrib.get("custom1", "").split("|") ch.schedules = [r.attrib["scheduleName"] for r in data.findall("{*}Role")] return ch def attribs(self): return { "forename": self.forename, "surname": self.surname, "middleName": self.middleName, "email": self.email, "phone": self.phone, "custom1": "|".join(self.levels).replace("&", "and"), "custom2": self.membershipWorksID, } def make_schedules(self, schedulesMap): roles = [ E.Role( { "roleID": self.cardholderID, "scheduleID": schedulesMap[schedule], "resourceID": "0", } ) for schedule in self.schedules ] return E.RoleSet( {"action": "UD", "roleSetID": self.cardholderID}, E.Roles(*roles) ) def make_credentials(self, newCredentials, cardFormats): out = [ E.Credential( { "formatName": str(credential.code[0]), "cardNumber": str(credential.code[1]), "formatID": cardFormats[str(credential.code[0])], "isCard": "true", "cardholderID": self.cardholderID, } ) for credential in newCredentials ] return E.Credentials({"action": "AD"}, *out) def update_door(door, members): cardFormats = door.get_cardFormats() cardholders = { member.membershipWorksID: member for member in [ DoorMember.from_cardholder(ch, door) for ch in door.get_cardholders() ] } schedulesMap = door.get_scheduleMap() allCredentials = set( Credential(hex=c.attrib["rawCardNumber"]) for c in door.get_credentials() ) # TODO: can I combine requests? for membershipworksMember in members: member = membershipworksMember.to_DoorMember(door) # cardholder did not exist, so add them if member.membershipWorksID not in cardholders: print("- Adding Member {member.forename} {member.surname}:") print(f" - {member.attribs()}") resp = door.doXMLRequest( ROOT(E.Cardholders({"action": "AD"}, E.Cardholder(member.attribs()))) ) member.cardholderID = resp.find("{*}Cardholders/{*}Cardholder").attrib[ "cardholderID" ] # create a dummy ch to force an update # TODO: probably a cleaner way to do this ch = copy.copy(member) ch.schedules = [] ch.credentials = set() # cardholder exists, compare contents else: ch = cardholders.pop(member.membershipWorksID) member.cardholderID = ch.cardholderID if member.attribs() != ch.attribs(): # update cardholder attributes print(f"- Updating profile for {member.forename} {member.surname}") print(f" - Old: {ch.attribs()}") print(f" - New: {member.attribs()}") door.doXMLRequest( ROOT( E.Cardholders( {"action": "UD", "cardholderID": member.cardholderID}, E.CardHolder(member.attribs()), ) ) ) if member.credentials != ch.credentials: print(f"- Updating card for {member.forename} {member.surname}") print(f" - {ch.credentials} -> {member.credentials}") oldCards = ch.credentials newCards = member.credentials allNewCards = set( card for m in members if m != membershipworksMember for card in m.credentials ) # cards removed, and won't be reassigned to someone else for card in (oldCards - newCards) - allNewCards: door.doXMLRequest( ROOT( E.Credentials( { "action": "UD", "rawCardNumber": card.hex, "isCard": "true", }, E.Credential({"cardholderID": ""}), ) ) ) if newCards - oldCards: # cards added for card in newCards & allNewCards: # new card exists in another member print( [ m for m in members for card in m.credentials if card in newCards ] ) raise Exception(f"Duplicate Card in input data! {card}") # card existed in door, and needs to be reassigned for card in newCards & allCredentials: door.doXMLRequest( ROOT( E.Credentials( { "action": "UD", "rawCardNumber": card.hex, "isCard": "true", }, E.Credential({"cardholderID": member.cardholderID}), ) ) ) # cards that never existed, and need to be created if newCards - allCredentials: door.doXMLRequest( ROOT( member.make_credentials( newCards - allCredentials, cardFormats ) ) ) if member.schedules != ch.schedules: print( "- Updating schedule for" + f" {member.forename} {member.surname}:" + f" {ch.schedules} -> {member.schedules}" ) door.doXMLRequest(ROOT(member.make_schedules(schedulesMap))) # TODO: delete cardholders that are no longer members? def main(): membershipworks_attributes = ( "_id,nam,phn,eml,lvl,lbl,xws,xms,xsc,xas,xfd,xac,xcf,xeh,xse,xlo" ) memberData = membershipworks.get_members( ["Members", "CMS Staff", "Misc. Access"], membershipworks_attributes ) members = [MembershipworksMember(m) for m in memberData] formerMemberData = membershipworks.get_members( ["Former Members"], membershipworks_attributes ) formerMembers = [ MembershipworksMember(m, formerMember=True) for m in formerMemberData ] for formerMember in formerMembers: member = next( ( m for m in members if m.membershipWorksID == formerMember.membershipWorksID ), None, ) # member exists in another folder if member is not None: member.formerMember = True else: # member is only a former member formerMember.formerMember = True members.append(formerMember) for door in doors.values(): print(door.name, door.ip) update_door(door, members) if __name__ == "__main__": main()