clicada/clicada/user/file.py

292 lines
8.1 KiB
Python
Raw Normal View History

# standard imports
import stat
import os
import logging
import json
import urllib.parse
2021-11-06 04:42:18 +01:00
import datetime
# external imports
from hexathon import strip_0x
from cic_types.condiments import MetadataPointer
from cic_types.models.person import Person
from cic_types.ext.requests import make_request
from cic_types.processor import generate_metadata_pointer
from requests.exceptions import HTTPError
import phonenumbers
# local imports
from clicada.encode import tx_normalize
from clicada.store.mem import MemDictStore
2022-01-21 11:03:01 +01:00
from clicada.error import (
ExpiredRecordError,
MetadataNotFoundError,
)
logg = logging.getLogger(__name__)
class Account(Person):
def __init__(self):
super(Account, self).__init__()
self.tags = []
def apply_custom(self, custom_data):
self.tags = custom_data['tags']
logg.debug('tags are now {}'.format(self.tags))
@classmethod
def deserialize(cls, person_data):
o = super(Account, cls).deserialize(person_data)
try:
o.tags = person_data['custom']['tags']
except KeyError as e:
pass
return o
def serialize(self):
o = super(Account, self).serialize()
o['custom'] = {}
o['custom']['tags'] = self.tags
return o
2021-11-07 03:52:52 +01:00
def __str__(self):
return '{} {} ({})'.format(
self.given_name,
self.family_name,
self.tel,
)
class FileUserStore:
2022-01-21 11:59:27 +01:00
def __init__(self, metadata_opener, chain_spec, label, store_base_path, ttl, encrypter=None):
2021-11-06 04:42:18 +01:00
invalidate_before = datetime.datetime.now() - datetime.timedelta(seconds=ttl)
self.invalidate_before = int(invalidate_before.timestamp())
2021-11-06 06:29:45 +01:00
self.have_xattr = False
self.label = label
self.store_path = os.path.join(
store_base_path,
chain_spec.arch(),
chain_spec.fork(),
str(chain_spec.network_id()),
self.label,
)
self.memstore_entity = MemDictStore()
os.makedirs(self.store_path, exist_ok=True)
self.__validate_dir()
self.metadata_opener = metadata_opener
2022-01-21 11:03:01 +01:00
self.failed_entities = {}
2022-01-21 11:59:27 +01:00
self.encrypter = encrypter
def __validate_dir(self):
st = os.stat(self.store_path)
if stat.S_ISDIR(st.st_mode):
logg.debug('using existing file store {} for {}'.format(self.store_path, self.label))
2022-01-21 11:03:01 +01:00
def is_dud(self, address):
return bool(self.failed_entities.get(address))
def put(self, k, v, force=False):
have_file = False
p = os.path.join(self.store_path, k)
2021-11-06 04:42:18 +01:00
its_time = True
try:
st = os.stat(p)
have_file = True
2021-11-06 04:42:18 +01:00
its_time = st[stat.ST_MTIME] < self.invalidate_before
except FileNotFoundError:
pass
2021-11-06 04:42:18 +01:00
if have_file and not its_time and not force:
raise FileExistsError('user resolution already exists for {}'.format(k))
2022-01-21 11:59:27 +01:00
ve = v
f = None
if self.encrypter != None:
ve = self.encrypter.encrypt(k, ve.encode('utf-8'))
f = open(p, 'wb')
else:
f = open(p, 'w')
f.write(ve)
f.close()
logg.info('added user store {} record {} -> {}'.format(self.label, k, v))
2021-11-06 06:29:45 +01:00
def stick(self, k):
p = os.path.join(self.store_path, k)
if self.have_xattr:
os.setxattr(p, 'cic_sticky', b'1')
else:
(s_h, s_t) = os.path.split(p)
sp = os.path.join(s_h, '.stick_' + s_t)
f = open(sp, 'w')
f.close()
def __is_sticky(self, p):
if self.have_xattr:
if not os.getxattr(p, 'cic_sticky'):
return False
else:
(s_h, s_t) = os.path.split(p)
sp = os.path.join(s_h, '.stick_' + s_t)
try:
os.stat(sp)
except FileNotFoundError:
return False
return True
def __unstick(self, p):
(s_h, s_t) = os.path.split(p)
sp = os.path.join(s_h, '.stick_' + s_t)
os.unlink(sp)
def path(self, k):
return os.path.join(self.store_path, k)
def sticky(self, k):
p = self.path(k)
return self.__is_sticky(p)
2021-11-06 04:42:18 +01:00
def check_expiry(self, p):
st = os.stat(p)
if st[stat.ST_MTIME] < self.invalidate_before:
2021-11-06 06:29:45 +01:00
if self.__is_sticky(p):
logg.debug('ignoring sticky record expiry for {}'.format(p))
return
2021-11-06 04:42:18 +01:00
raise ExpiredRecordError('record in {} is expired'.format(p))
2021-11-06 06:29:45 +01:00
def get(self, k, ignore_expired=False):
try:
p = os.path.join(self.store_path, k)
except FileNotFoundError as e:
if self.__is_sticky(p):
logg.warning('removed orphaned sticky record for ' + p)
self.__unstick(p)
2021-11-06 04:42:18 +01:00
self.check_expiry(p)
2022-01-21 11:59:27 +01:00
f = None
if self.encrypter != None:
f = open(p, 'rb')
else:
f = open(p, 'r')
v = f.read()
f.close()
2022-01-21 11:59:27 +01:00
if self.encrypter != None:
v = self.encrypter.decrypt(k, v)
logg.debug('>>>>>>>>>>>>< v decoded {}'.format(v))
v = v.decode('utf-8')
logg.debug('retrieved {} from {}'.format(k, p))
2022-01-21 11:59:27 +01:00
return v.strip()
2021-11-06 04:42:18 +01:00
def by_phone(self, phone, update=False):
phone = phonenumbers.parse(phone, None)
phone = phonenumbers.format_number(phone, phonenumbers.PhoneNumberFormat.E164)
phone_file = phone.replace('+', '')
2021-11-06 06:29:45 +01:00
ignore_expired = self.sticky(phone_file)
2021-11-06 04:42:18 +01:00
if not update:
try:
2021-11-06 06:29:45 +01:00
v = self.get(phone_file, ignore_expired=ignore_expired)
2021-11-06 04:42:18 +01:00
return v
except FileNotFoundError:
pass
except ExpiredRecordError as e:
logg.info(e)
pass
2021-11-07 03:52:52 +01:00
getter = self.metadata_opener
ptr = generate_metadata_pointer(phone.encode('utf-8'), MetadataPointer.PHONE)
r = None
2021-11-06 04:42:18 +01:00
user_address = None
try:
2021-11-07 03:52:52 +01:00
r = getter.open(ptr)
user_address = json.loads(r)
except HTTPError as e:
logg.debug('no address found for phone {}: {}'.format(phone, e))
2021-11-06 04:42:18 +01:00
return None
self.put(phone_file, user_address, force=update)
2021-11-06 04:42:18 +01:00
return user_address
2022-01-21 11:03:01 +01:00
def metadata_to_person(self, v):
person = Account()
try:
person_data = person.deserialize(person_data=v)
except Exception as e:
person_data = v
return person_data
def by_address(self, address, update=False):
2022-01-21 11:03:01 +01:00
address = tx_normalize.wallet_address(address)
address = strip_0x(address)
#if self.failed_entities.get(address):
if self.is_dud(address):
logg.debug('already tried and failed {}, skipping'.format(address))
return address
2022-01-21 11:03:01 +01:00
2021-11-06 06:29:45 +01:00
ignore_expired = self.sticky(address)
2021-11-06 04:42:18 +01:00
if not update:
try:
2021-11-06 06:29:45 +01:00
v = self.get(address, ignore_expired=ignore_expired)
2021-11-06 04:42:18 +01:00
v = json.loads(v)
2022-01-21 11:03:01 +01:00
return self.metadata_to_person(v)
2021-11-06 04:42:18 +01:00
except FileNotFoundError:
pass
except ExpiredRecordError as e:
logg.info(e)
pass
getter = self.metadata_opener
ptr = generate_metadata_pointer(bytes.fromhex(address), MetadataPointer.PERSON)
r = None
try:
r = getter.open(ptr)
except Exception as e:
logg.debug('no metadata found for {}: {}'.format(address, e))
2022-01-21 11:03:01 +01:00
2022-04-27 11:43:22 +02:00
if not r:
2022-01-21 11:03:01 +01:00
self.failed_entities[address] = True
raise MetadataNotFoundError()
data = json.loads(r)
person = Account()
person_data = person.deserialize(person_data=data)
ptr = generate_metadata_pointer(bytes.fromhex(address), MetadataPointer.CUSTOM)
r = None
try:
r = getter.open(ptr)
o = json.loads(r)
person_data.apply_custom(o)
except Exception as e:
pass
self.put(address, json.dumps(person_data.serialize()), force=update)
return person_data