From 86344f54d54f3ea2b4e937a85fc7d6fd40d1bb5d Mon Sep 17 00:00:00 2001 From: Moky Date: Mon, 30 Oct 2023 00:15:06 +0800 Subject: [PATCH] dimp 1.0.2: define Visa & Bulletin DocumentHelper; BroadcastHelper --- dimp/__init__.py | 3 + dimp/barrack.py | 97 ++++++---------------- dimp/crypto/keys.py | 4 + dimp/crypto/pnf.py | 11 +-- dimp/crypto/ted.py | 8 ++ dimp/dkd/__init__.py | 2 +- dimp/dkd/contents.py | 6 +- dimp/dkd/factory.py | 3 +- dimp/dkd/group_admins.py | 106 ++++++++++++++++++++++++ dimp/dkd/groups.py | 69 +--------------- dimp/dkd/receipt.py | 10 +-- dimp/mkm/__init__.py | 14 ++-- dimp/mkm/docs.py | 18 ++-- dimp/mkm/entity.py | 92 +++++++++++++++++++-- dimp/mkm/entity_impl.py | 109 ------------------------- dimp/mkm/group.py | 53 +++++++++++- dimp/mkm/group_impl.py | 85 ------------------- dimp/mkm/helper.py | 168 ++++++++++++++++++++++++++++++++++++++ dimp/mkm/user.py | 103 ++++++++++++++++++++++- dimp/mkm/user_impl.py | 135 ------------------------------ dimp/msg/base.py | 1 - dimp/msg/envelope.py | 31 ++----- dimp/msg/instant.py | 3 +- dimp/msg/reliable.py | 29 ------- dimp/protocol/__init__.py | 3 + dimp/protocol/docs.py | 124 ++++++++++++++++++++++++++++ dimp/protocol/files.py | 11 +-- dimp/protocol/money.py | 6 +- setup.py | 6 +- 29 files changed, 736 insertions(+), 574 deletions(-) create mode 100644 dimp/dkd/group_admins.py delete mode 100644 dimp/mkm/entity_impl.py delete mode 100644 dimp/mkm/group_impl.py create mode 100644 dimp/mkm/helper.py delete mode 100644 dimp/mkm/user_impl.py create mode 100644 dimp/protocol/docs.py diff --git a/dimp/__init__.py b/dimp/__init__.py index e969acd..75d3a36 100644 --- a/dimp/__init__.py +++ b/dimp/__init__.py @@ -88,6 +88,8 @@ 'User', 'UserDataSource', 'BaseUser', 'Group', 'GroupDataSource', 'BaseGroup', + 'DocumentHelper', 'BroadcastHelper', # 'thanos', + # # DaoKeDao # @@ -141,4 +143,5 @@ # Core # 'Barrack', 'Transceiver', 'Packer', 'Processor', + ] diff --git a/dimp/barrack.py b/dimp/barrack.py index 42e5d02..9e99bb3 100644 --- a/dimp/barrack.py +++ b/dimp/barrack.py @@ -41,8 +41,12 @@ from typing import Optional, List from mkm.crypto import EncryptKey, VerifyKey -from mkm import EntityType, ID, ANYONE, FOUNDER -from mkm import Visa, Bulletin +from mkm import EntityType, ID + +from .protocol import Visa, Bulletin + +from .mkm.helper import thanos +from .mkm.helper import DocumentHelper, BroadcastHelper from .mkm import User, Group from .mkm import EntityDelegate, UserDataSource, GroupDataSource @@ -102,17 +106,26 @@ def create_group(self, identifier: ID) -> Optional[Group]: # protected def visa_key(self, identifier: ID) -> Optional[EncryptKey]: - visa = self.document(identifier=identifier) - if isinstance(visa, Visa): - if visa.valid: - return visa.public_key + visa = self.visa(identifier=identifier) + if visa is not None: # and visa.valid: + return visa.public_key # protected def meta_key(self, identifier: ID) -> Optional[VerifyKey]: meta = self.meta(identifier=identifier) - if meta is not None: + if meta is not None: # and meta.valid: return meta.public_key + def visa(self, identifier: ID) -> Optional[Visa]: + """ Get last visa document """ + documents = self.documents(identifier=identifier) + return DocumentHelper.last_visa(documents=documents) + + def bulletin(self, identifier: ID) -> Optional[Bulletin]: + """ get last bulletin document """ + documents = self.documents(identifier=identifier) + return DocumentHelper.last_bulletin(documents=documents) + # # Entity Delegate # @@ -184,10 +197,10 @@ def founder(self, identifier: ID) -> Optional[ID]: # check for broadcast if identifier.is_broadcast: # founder of broadcast group - return broadcast_founder(group=identifier) + return BroadcastHelper.broadcast_founder(group=identifier) # get from document - doc = self.document(identifier=identifier) - if isinstance(doc, Bulletin): + doc = self.bulletin(identifier=identifier) + if doc is not None: # and doc.valid: return doc.founder # TODO: load founder from database @@ -196,7 +209,7 @@ def owner(self, identifier: ID) -> Optional[ID]: # check for broadcast if identifier.is_broadcast: # owner of broadcast group - return broadcast_owner(group=identifier) + return BroadcastHelper.broadcast_owner(group=identifier) # check group type if identifier.type == EntityType.GROUP: # Polylogue's owner is its founder @@ -208,72 +221,16 @@ def members(self, identifier: ID) -> List[ID]: # check for broadcast if identifier.is_broadcast: # members of broadcast group - return broadcast_members(group=identifier) + return BroadcastHelper.broadcast_members(group=identifier) # TODO: load members from database return [] # Override def assistants(self, identifier: ID) -> List[ID]: - doc = self.document(identifier=identifier) - if isinstance(doc, Bulletin) and doc.valid: + doc = self.bulletin(identifier=identifier) + if doc is not None: # and doc.valid: bots = doc.assistants if bots is not None: return bots # TODO: get group bots from SP configuration return [] - - -def group_seed(group: ID) -> Optional[str]: - name = group.name - if name is not None: - length = len(name) - if length > 0 and (length != 8 or name.lower() != 'everyone'): - return name - - -def broadcast_founder(group: ID) -> Optional[ID]: - name = group_seed(group=group) - if name is None: - # Consensus: the founder of group 'everyone@everywhere' - # 'Albert Moky' - return FOUNDER - else: - # DISCUSS: who should be the founder of group 'xxx@everywhere'? - # 'anyone@anywhere', or 'xxx.founder@anywhere' - return ID.parse(identifier=name + '.founder@anywhere') - - -def broadcast_owner(group: ID) -> Optional[ID]: - name = group_seed(group=group) - if name is None: - # Consensus: the owner of group 'everyone@everywhere' - # 'anyone@anywhere' - return ANYONE - else: - # DISCUSS: who should be the owner of group 'xxx@everywhere'? - # 'anyone@anywhere', or 'xxx.owner@anywhere' - return ID.parse(identifier=name + '.owner@anywhere') - - -def broadcast_members(group: ID) -> List[ID]: - name = group_seed(group=group) - if name is None: - # Consensus: the member of group 'everyone@everywhere' - # 'anyone@anywhere' - return [ANYONE] - else: - # DISCUSS: who should be the member of group 'xxx@everywhere'? - # 'anyone@anywhere', or 'xxx.member@anywhere' - owner = ID.parse(identifier=name + '.owner@anywhere') - member = ID.parse(identifier=name + '.member@anywhere') - return [owner, member] - - -def thanos(planet: dict, finger: int) -> int: - """ Thanos can kill half lives of a world with a snap of the finger """ - people = planet.keys() - for anybody in people: - if (++finger & 1) == 1: - # kill it - planet.pop(anybody) - return finger diff --git a/dimp/crypto/keys.py b/dimp/crypto/keys.py index add60b2..4b6e184 100644 --- a/dimp/crypto/keys.py +++ b/dimp/crypto/keys.py @@ -64,16 +64,19 @@ def get_key_algorithm(cls, key: Dict[str, Any]) -> Optional[str]: @classmethod def keys_match(cls, encrypt_key: EncryptKey, decrypt_key: DecryptKey) -> bool: + """ match encrypt key """ gf = CryptographyKeyFactoryManager.general_factory return gf.keys_match(encrypt_key=encrypt_key, decrypt_key=decrypt_key) @classmethod def asymmetric_keys_match(cls, sign_key: SignKey, verify_key: VerifyKey) -> bool: + """ match sign key """ gf = CryptographyKeyFactoryManager.general_factory return gf.asymmetric_keys_match(sign_key=sign_key, verify_key=verify_key) @classmethod def keys_equal(cls, a: SymmetricKey, b: SymmetricKey) -> bool: + """ symmetric key equals """ if a is b: # same object return True @@ -82,6 +85,7 @@ def keys_equal(cls, a: SymmetricKey, b: SymmetricKey) -> bool: @classmethod def private_keys_equal(cls, a: PrivateKey, b: PrivateKey) -> bool: + """ asymmetric key equals """ if a is b: # same object return True diff --git a/dimp/crypto/pnf.py b/dimp/crypto/pnf.py index 1a643d4..8374d51 100644 --- a/dimp/crypto/pnf.py +++ b/dimp/crypto/pnf.py @@ -70,10 +70,11 @@ def __init__(self, dictionary: Dict[str, Any]): @property def data(self) -> Optional[TransportableData]: - if self.__attachment is None: + ted = self.__attachment + if ted is None: base64 = self.get('data') - self.__attachment = TransportableData.parse(base64) - return self.__attachment + self.__attachment = ted = TransportableData.parse(base64) + return ted @data.setter def data(self, ted: Optional[TransportableData]): @@ -103,7 +104,7 @@ def filename(self) -> Optional[str]: @filename.setter def filename(self, name: Optional[str]): - if name is None: + if name is None: # or len(name) == 0: self.pop('filename', None) else: self['filename'] = name @@ -125,7 +126,7 @@ def url(self, remote: Optional[URI]): if remote is None: self.pop('URL', None) else: - # TODO: convert URI to str + # convert URI to str self['URL'] = remote self.__url = remote diff --git a/dimp/crypto/ted.py b/dimp/crypto/ted.py index 630d419..e128aa4 100644 --- a/dimp/crypto/ted.py +++ b/dimp/crypto/ted.py @@ -58,6 +58,14 @@ def __init__(self, dictionary: Dict[str, Any]): # binary data self.__data: Optional[bytes] = None + # Override + def __len__(self) -> int: + """ return len(self) """ + # if super().__len__() == 0: + # return 0 + data = self.data + return 0 if data is None else len(data) + # Override def __str__(self) -> str: text = self.get_str(key='data', default='') diff --git a/dimp/dkd/__init__.py b/dimp/dkd/__init__.py index 7ddd71d..ff9c704 100644 --- a/dimp/dkd/__init__.py +++ b/dimp/dkd/__init__.py @@ -44,7 +44,7 @@ from .groups import BaseHistoryCommand, BaseGroupCommand from .groups import InviteGroupCommand, ExpelGroupCommand, JoinGroupCommand from .groups import QuitGroupCommand, QueryGroupCommand, ResetGroupCommand -from .groups import HireGroupCommand, FireGroupCommand, ResignGroupCommand +from .group_admins import HireGroupCommand, FireGroupCommand, ResignGroupCommand from .factory import CommandGeneralFactory, CommandFactoryManager diff --git a/dimp/dkd/contents.py b/dimp/dkd/contents.py index fb0fe41..0db9be2 100644 --- a/dimp/dkd/contents.py +++ b/dimp/dkd/contents.py @@ -318,5 +318,9 @@ def name(self) -> str: def avatar(self) -> Optional[PortableNetworkFile]: if self.__avatar is None: url = self.get('avatar') - self.__avatar = PortableNetworkFile.parse(url) + if isinstance(url, str) and len(url) == 0: + # ignore empty URL + pass + else: + self.__avatar = PortableNetworkFile.parse(url) return self.__avatar diff --git a/dimp/dkd/factory.py b/dimp/dkd/factory.py index 42b640c..d1a1511 100644 --- a/dimp/dkd/factory.py +++ b/dimp/dkd/factory.py @@ -50,7 +50,8 @@ def get_command_factory(self, cmd: str) -> Optional[CommandFactory]: # noinspection PyMethodMayBeStatic def get_cmd(self, content: Dict[str, Any], default: Optional[str]) -> Optional[str]: - return Converter.get_str(value=content.get('command'), default=default) + cmd = content.get('command') + return Converter.get_str(value=cmd, default=default) def parse_command(self, content: Any) -> Optional[Command]: if content is None: diff --git a/dimp/dkd/group_admins.py b/dimp/dkd/group_admins.py new file mode 100644 index 0000000..37df5d2 --- /dev/null +++ b/dimp/dkd/group_admins.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +# +# DIMP : Decentralized Instant Messaging Protocol +# +# Written in 2023 by Moky +# +# ============================================================================== +# MIT License +# +# Copyright (c) 2023 Albert Moky +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================== + + +from typing import Optional, Any, Dict, List + +from mkm import ID + +from ..protocol import GroupCommand +from ..protocol import HireCommand, FireCommand, ResignCommand + +from .groups import BaseGroupCommand + + +""" + Administrator + ~~~~~~~~~~~~~ +""" + + +class HireGroupCommand(BaseGroupCommand, HireCommand): + + def __init__(self, content: Dict[str, Any] = None, group: ID = None, + administrators: List[ID] = None, + assistants: List[ID] = None): + cmd = GroupCommand.HIRE if content is None else None + super().__init__(content, cmd=cmd, group=group) + # group admins + if administrators is not None: + self['administrators'] = ID.revert(administrators) + # group bots + if assistants is not None: + self['assistants'] = ID.revert(assistants) + + @property # Override + def administrators(self) -> Optional[List[ID]]: + users = self.get('administrators') + if users is not None: + return ID.convert(users) + + @property # Override + def assistants(self) -> Optional[List[ID]]: + bots = self.get('assistants') + if bots is not None: + return ID.convert(bots) + + +class FireGroupCommand(BaseGroupCommand, FireCommand): + + def __init__(self, content: Dict[str, Any] = None, group: ID = None, + administrators: List[ID] = None, + assistants: List[ID] = None): + cmd = GroupCommand.FIRE if content is None else None + super().__init__(content=content, cmd=cmd, group=group) + # group admins + if administrators is not None: + self['administrators'] = ID.revert(administrators) + # group bots + if assistants is not None: + self['assistants'] = ID.revert(assistants) + + @property # Override + def administrators(self) -> Optional[List[ID]]: + users = self.get('administrators') + if users is not None: + return ID.convert(users) + + @property # Override + def assistants(self) -> Optional[List[ID]]: + bots = self.get('assistants') + if bots is not None: + return ID.convert(bots) + + +class ResignGroupCommand(BaseGroupCommand, ResignCommand): + + def __init__(self, content: Dict[str, Any] = None, group: ID = None): + cmd = GroupCommand.RESIGN if content is None else None + super().__init__(content=content, cmd=cmd, group=group) diff --git a/dimp/dkd/groups.py b/dimp/dkd/groups.py index c15d159..fda63e2 100644 --- a/dimp/dkd/groups.py +++ b/dimp/dkd/groups.py @@ -44,7 +44,6 @@ from ..protocol import HistoryCommand, GroupCommand from ..protocol import InviteCommand, ExpelCommand, JoinCommand, QuitCommand, QueryCommand, ResetCommand -from ..protocol import HireCommand, FireCommand, ResignCommand from .base import BaseCommand @@ -154,6 +153,7 @@ def __init__(self, content: Dict[str, Any] = None, class ExpelGroupCommand(BaseGroupCommand, ExpelCommand): + """ Deprecated, use 'reset' instead """ def __init__(self, content: Dict[str, Any] = None, group: ID = None, member: ID = None, members: List[ID] = None): @@ -225,70 +225,3 @@ def __init__(self, content: Dict[str, Any] = None, group: ID = None, members: Li """ cmd = GroupCommand.RESET if content is None else None super().__init__(content, cmd=cmd, group=group, members=members) - - -""" - Administrator - ~~~~~~~~~~~~~ -""" - - -class HireGroupCommand(BaseGroupCommand, HireCommand): - - def __init__(self, content: Dict[str, Any] = None, group: ID = None, - administrators: List[ID] = None, - assistants: List[ID] = None): - cmd = GroupCommand.HIRE if content is None else None - super().__init__(content, cmd=cmd, group=group) - # group admins - if administrators is not None: - self['administrators'] = ID.revert(administrators) - # group bots - if assistants is not None: - self['assistants'] = ID.revert(assistants) - - @property # Override - def administrators(self) -> Optional[List[ID]]: - users = self.get('administrators') - if users is not None: - return ID.convert(users) - - @property # Override - def assistants(self) -> Optional[List[ID]]: - bots = self.get('assistants') - if bots is not None: - return ID.convert(bots) - - -class FireGroupCommand(BaseGroupCommand, FireCommand): - - def __init__(self, content: Dict[str, Any] = None, group: ID = None, - administrators: List[ID] = None, - assistants: List[ID] = None): - cmd = GroupCommand.FIRE if content is None else None - super().__init__(content=content, cmd=cmd, group=group) - # group admins - if administrators is not None: - self['administrators'] = ID.revert(administrators) - # group bots - if assistants is not None: - self['assistants'] = ID.revert(assistants) - - @property # Override - def administrators(self) -> Optional[List[ID]]: - users = self.get('administrators') - if users is not None: - return ID.convert(users) - - @property # Override - def assistants(self) -> Optional[List[ID]]: - bots = self.get('assistants') - if bots is not None: - return ID.convert(bots) - - -class ResignGroupCommand(BaseGroupCommand, ResignCommand): - - def __init__(self, content: Dict[str, Any] = None, group: ID = None): - cmd = GroupCommand.RESIGN if content is None else None - super().__init__(content=content, cmd=cmd, group=group) diff --git a/dimp/dkd/receipt.py b/dimp/dkd/receipt.py index adad8a4..c3563b9 100644 --- a/dimp/dkd/receipt.py +++ b/dimp/dkd/receipt.py @@ -35,17 +35,19 @@ As receipt returned to sender to proofing the message's received """ +from abc import ABC from typing import Optional, Any, Dict from mkm.types import Converter -from dkd import Envelope, InstantMessage +from dkd import Envelope from ..protocol import Command from ..protocol import ReceiptCommand, ReceiptCommandMixIn from .commands import BaseCommand -class BaseReceipt(BaseCommand, ReceiptCommand): +# noinspection PyAbstractClass +class BaseReceipt(BaseCommand, ReceiptCommand, ABC): """ Receipt Command ~~~~~~~~~~~~~~~ @@ -125,10 +127,6 @@ def original_signature(self) -> Optional[str]: signature = origin.get('signature') return Converter.get_str(value=signature, default=None) - # Override - def match_message(self, msg: InstantMessage) -> bool: - raise NotImplemented - class BaseReceiptCommand(BaseReceipt, ReceiptCommandMixIn): diff --git a/dimp/mkm/__init__.py b/dimp/mkm/__init__.py index a257dff..d3634f4 100644 --- a/dimp/mkm/__init__.py +++ b/dimp/mkm/__init__.py @@ -30,18 +30,18 @@ from mkm import * +from ..protocol import Visa, Bulletin + from .meta import BaseMeta, MetaHelper from .document import BaseDocument from .docs import BaseVisa, BaseBulletin from .delegate import EntityDelegate -from .entity import Entity, EntityDataSource -from .user import User, UserDataSource -from .group import Group, GroupDataSource +from .entity import Entity, EntityDataSource, BaseEntity +from .user import User, UserDataSource, BaseUser +from .group import Group, GroupDataSource, BaseGroup -from .entity_impl import BaseEntity -from .user_impl import BaseUser -from .group_impl import BaseGroup +from .helper import DocumentHelper, BroadcastHelper # , thanos __all__ = [ @@ -76,4 +76,6 @@ 'User', 'UserDataSource', 'BaseUser', 'Group', 'GroupDataSource', 'BaseGroup', + 'DocumentHelper', 'BroadcastHelper', # 'thanos', + ] diff --git a/dimp/mkm/docs.py b/dimp/mkm/docs.py index 912b441..f62f337 100644 --- a/dimp/mkm/docs.py +++ b/dimp/mkm/docs.py @@ -35,7 +35,8 @@ from mkm.format import PortableNetworkFile from mkm import ID from mkm import Document -from mkm import Visa, Bulletin + +from ..protocol import Visa, Bulletin from .document import BaseDocument @@ -92,7 +93,11 @@ def public_key(self, key: EncryptKey): def avatar(self) -> Optional[PortableNetworkFile]: if self.__avatar is None: url = self.get_property(key='avatar') - self.__avatar = PortableNetworkFile.parse(url) + if isinstance(url, str) and len(url) == 0: + # ignore empty URL + pass + else: + self.__avatar = PortableNetworkFile.parse(url) return self.__avatar @avatar.setter # Override @@ -122,7 +127,8 @@ def __init__(self, document: Optional[Dict[str, Any]] = None, @property # Override def founder(self) -> Optional[ID]: - return ID.parse(identifier=self.get_property(key='founder')) + identifier = self.get_property(key='founder') + return ID.parse(identifier=identifier) @property # Override def assistants(self) -> Optional[List[ID]]: @@ -130,9 +136,9 @@ def assistants(self) -> Optional[List[ID]]: bots = self.get_property(key='assistants') if bots is None: # get from 'assistant' - ass = self.get_property(key='assistant') - bot = ID.parse(identifier=ass) - self.__bots = [] if bot is None else [bot] + single = self.get_property(key='assistant') + single = ID.parse(identifier=single) + self.__bots = [] if single is None else [single] else: self.__bots = ID.convert(bots) return self.__bots diff --git a/dimp/mkm/entity.py b/dimp/mkm/entity.py index b934d96..e517c7e 100644 --- a/dimp/mkm/entity.py +++ b/dimp/mkm/entity.py @@ -28,8 +28,9 @@ # SOFTWARE. # ============================================================================== +import weakref from abc import ABC, abstractmethod -from typing import Optional +from typing import Optional, List from mkm import ID, Meta, Document @@ -49,21 +50,20 @@ class EntityDataSource(ABC): @abstractmethod def meta(self, identifier: ID) -> Optional[Meta]: """ - Get meta for entity ID + Get meta for entity :param identifier: entity ID - :return: Meta + :return: Meta object """ raise NotImplemented @abstractmethod - def document(self, identifier: ID, doc_type: str = '*') -> Optional[Document]: + def documents(self, identifier: ID) -> List[Document]: """ - Get document for entity ID + Get documents for entity ID :param identifier: entity ID - :param doc_type: document type - :return: Document + :return: Document list """ raise NotImplemented @@ -108,7 +108,81 @@ def meta(self) -> Meta: """ Get meta """ raise NotImplemented + @property @abstractmethod - def document(self, doc_type: str = '*') -> Optional[Document]: - """ Get document with type """ + def documents(self) -> List[Document]: + """ Get documents """ raise NotImplemented + + +class BaseEntity(Entity): + + def __init__(self, identifier: ID): + """ + Create Entity with ID + + :param identifier: User/Group ID + """ + super().__init__() + self.__id = identifier + self.__barrack = None + + # Override + def __str__(self): + """ Return str(self). """ + clazz = self.__class__.__name__ + identifier = self.identifier + network = identifier.address.type + return '<%s id="%s" network=%d />' % (clazz, identifier, network) + + # Override + def __eq__(self, other) -> bool: + """ Return self==value. """ + if isinstance(other, Entity): + if self is other: + # same object + return True + other = other.identifier + # check with ID + return self.__id.__eq__(other) + + # Override + def __ne__(self, other) -> bool: + """ Return self!=value. """ + if isinstance(other, Entity): + if self is other: + # same object + return False + other = other.identifier + # check with ID + return self.__id.__ne__(other) + + @property # Override + def data_source(self) -> Optional[EntityDataSource]: + if self.__barrack is not None: + return self.__barrack() + + @data_source.setter # Override + def data_source(self, barrack: EntityDataSource): + self.__barrack = weakref.ref(barrack) + + @property # Override + def identifier(self) -> ID: + return self.__id + + @property # Override + def type(self) -> int: + """ Entity type """ + return self.__id.type + + @property # Override + def meta(self) -> Meta: + delegate = self.data_source + # assert delegate is not None, 'entity delegate not set yet' + return delegate.meta(identifier=self.__id) + + @property # Override + def documents(self) -> List[Document]: + delegate = self.data_source + # assert delegate is not None, 'entity delegate not set yet' + return delegate.documents(identifier=self.__id) diff --git a/dimp/mkm/entity_impl.py b/dimp/mkm/entity_impl.py deleted file mode 100644 index c160e13..0000000 --- a/dimp/mkm/entity_impl.py +++ /dev/null @@ -1,109 +0,0 @@ -# -*- coding: utf-8 -*- -# -# DIMP : Decentralized Instant Messaging Protocol -# -# Written in 2019 by Moky -# -# ============================================================================== -# MIT License -# -# Copyright (c) 2019 Albert Moky -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -# ============================================================================== - -import weakref -from typing import Optional - -from mkm import ID, Meta, Document - -from .entity import Entity, EntityDataSource - - -class BaseEntity(Entity): - - def __init__(self, identifier: ID): - """ - Create Entity with ID - - :param identifier: User/Group ID - """ - super().__init__() - self.__id = identifier - self.__barrack = None - - # Override - def __str__(self): - """ Return str(self). """ - clazz = self.__class__.__name__ - identifier = self.identifier - network = identifier.address.type - return '<%s id="%s" network=%d />' % (clazz, identifier, network) - - # Override - def __eq__(self, other) -> bool: - """ Return self==value. """ - if isinstance(other, Entity): - if self is other: - # same object - return True - other = other.identifier - # check with ID - return self.__id.__eq__(other) - - # Override - def __ne__(self, other) -> bool: - """ Return self!=value. """ - if isinstance(other, Entity): - if self is other: - # same object - return False - other = other.identifier - # check with ID - return self.__id.__ne__(other) - - @property # Override - def data_source(self) -> Optional[EntityDataSource]: - if self.__barrack is not None: - return self.__barrack() - - @data_source.setter # Override - def data_source(self, barrack: EntityDataSource): - self.__barrack = weakref.ref(barrack) - - @property # Override - def identifier(self) -> ID: - return self.__id - - @property # Override - def type(self) -> int: - """ Entity type """ - return self.__id.type - - @property # Override - def meta(self) -> Meta: - delegate = self.data_source - # assert delegate is not None, 'entity delegate not set yet' - return delegate.meta(identifier=self.__id) - - # Override - def document(self, doc_type: str = '*') -> Optional[Document]: - delegate = self.data_source - # assert delegate is not None, 'entity delegate not set yet' - return delegate.document(identifier=self.__id, doc_type=doc_type) diff --git a/dimp/mkm/group.py b/dimp/mkm/group.py index 243656f..bb698df 100644 --- a/dimp/mkm/group.py +++ b/dimp/mkm/group.py @@ -31,9 +31,12 @@ from abc import ABC, abstractmethod from typing import Optional, List -from mkm import ID, Bulletin +from mkm import ID -from .entity import EntityDataSource, Entity +from ..protocol import Bulletin + +from .helper import DocumentHelper +from .entity import EntityDataSource, Entity, BaseEntity class GroupDataSource(EntityDataSource, ABC): @@ -139,3 +142,49 @@ def members(self) -> List[ID]: @abstractmethod def assistants(self) -> List[ID]: raise NotImplemented + + +class BaseGroup(BaseEntity, Group): + + def __init__(self, identifier: ID): + super().__init__(identifier=identifier) + # once the group founder is set, it will never change + self.__founder = None + + @BaseEntity.data_source.getter # Override + def data_source(self) -> Optional[GroupDataSource]: + return super().data_source + + # @data_source.setter # Override + # def data_source(self, delegate: GroupDataSource): + # super(BaseGroup, BaseGroup).data_source.__set__(self, delegate) + + @property # Override + def bulletin(self) -> Optional[Bulletin]: + return DocumentHelper.last_bulletin(documents=self.documents) + + @property # Override + def founder(self) -> ID: + if self.__founder is None: + delegate = self.data_source + # assert delegate is not None, 'group delegate not set yet' + self.__founder = delegate.founder(identifier=self.identifier) + return self.__founder + + @property # Override + def owner(self) -> ID: + delegate = self.data_source + # assert delegate is not None, 'group delegate not set yet' + return delegate.owner(identifier=self.identifier) + + @property # Override + def members(self) -> List[ID]: + delegate = self.data_source + # assert delegate is not None, 'group delegate not set yet' + return delegate.members(identifier=self.identifier) + + @property # Override + def assistants(self) -> List[ID]: + delegate = self.data_source + # assert delegate is not None, 'group delegate not set yet' + return delegate.assistants(identifier=self.identifier) diff --git a/dimp/mkm/group_impl.py b/dimp/mkm/group_impl.py deleted file mode 100644 index 6f64346..0000000 --- a/dimp/mkm/group_impl.py +++ /dev/null @@ -1,85 +0,0 @@ -# -*- coding: utf-8 -*- -# -# DIMP : Decentralized Instant Messaging Protocol -# -# Written in 2019 by Moky -# -# ============================================================================== -# MIT License -# -# Copyright (c) 2019 Albert Moky -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -# ============================================================================== - -from typing import Optional, List - -from mkm import ID, Bulletin - -from .group import Group, GroupDataSource -from .entity_impl import BaseEntity - - -class BaseGroup(BaseEntity, Group): - - def __init__(self, identifier: ID): - super().__init__(identifier=identifier) - # once the group founder is set, it will never change - self.__founder = None - - @BaseEntity.data_source.getter # Override - def data_source(self) -> Optional[GroupDataSource]: - return super().data_source - - # @data_source.setter # Override - # def data_source(self, delegate: GroupDataSource): - # super(BaseGroup, BaseGroup).data_source.__set__(self, delegate) - - @property # Override - def bulletin(self) -> Optional[Bulletin]: - doc = self.document() - if isinstance(doc, Bulletin): - return doc - assert doc is None, 'group document error: %s' % doc - - @property # Override - def founder(self) -> ID: - if self.__founder is None: - delegate = self.data_source - # assert delegate is not None, 'group delegate not set yet' - self.__founder = delegate.founder(identifier=self.identifier) - return self.__founder - - @property # Override - def owner(self) -> ID: - delegate = self.data_source - # assert delegate is not None, 'group delegate not set yet' - return delegate.owner(identifier=self.identifier) - - @property # Override - def members(self) -> List[ID]: - delegate = self.data_source - # assert delegate is not None, 'group delegate not set yet' - return delegate.members(identifier=self.identifier) - - @property # Override - def assistants(self) -> List[ID]: - delegate = self.data_source - # assert delegate is not None, 'group delegate not set yet' - return delegate.assistants(identifier=self.identifier) diff --git a/dimp/mkm/helper.py b/dimp/mkm/helper.py new file mode 100644 index 0000000..cdaedd4 --- /dev/null +++ b/dimp/mkm/helper.py @@ -0,0 +1,168 @@ +# -*- coding: utf-8 -*- +# +# DIMP : Decentralized Instant Messaging Protocol +# +# Written in 2023 by Moky +# +# ============================================================================== +# MIT License +# +# Copyright (c) 2023 Albert Moky +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================== + +from abc import ABC +from typing import Optional, List + +from mkm.types import DateTime +from mkm import ID, ANYONE, FOUNDER +from mkm import Document + +from ..protocol import Visa, Bulletin + + +def thanos(planet: dict, finger: int) -> int: + """ Thanos can kill half lives of a world with a snap of the finger """ + people = planet.keys() + for anybody in people: + if (++finger & 1) == 1: + # kill it + planet.pop(anybody) + return finger + + +class BroadcastHelper(ABC): + + @classmethod # private + def group_seed(cls, group: ID) -> Optional[str]: + name = group.name + if name is not None: + length = len(name) + if length > 0 and (length != 8 or name.lower() != 'everyone'): + return name + + @classmethod # protected + def broadcast_founder(cls, group: ID) -> Optional[ID]: + name = cls.group_seed(group=group) + if name is None: + # Consensus: the founder of group 'everyone@everywhere' + # 'Albert Moky' + return FOUNDER + else: + # DISCUSS: who should be the founder of group 'xxx@everywhere'? + # 'anyone@anywhere', or 'xxx.founder@anywhere' + return ID.parse(identifier=name + '.founder@anywhere') + + @classmethod # protected + def broadcast_owner(cls, group: ID) -> Optional[ID]: + name = cls.group_seed(group=group) + if name is None: + # Consensus: the owner of group 'everyone@everywhere' + # 'anyone@anywhere' + return ANYONE + else: + # DISCUSS: who should be the owner of group 'xxx@everywhere'? + # 'anyone@anywhere', or 'xxx.owner@anywhere' + return ID.parse(identifier=name + '.owner@anywhere') + + @classmethod # protected + def broadcast_members(cls, group: ID) -> List[ID]: + name = cls.group_seed(group=group) + if name is None: + # Consensus: the member of group 'everyone@everywhere' + # 'anyone@anywhere' + return [ANYONE] + else: + # DISCUSS: who should be the member of group 'xxx@everywhere'? + # 'anyone@anywhere', or 'xxx.member@anywhere' + owner = ID.parse(identifier=name + '.owner@anywhere') + member = ID.parse(identifier=name + '.member@anywhere') + return [owner, member] + + +class DocumentHelper(ABC): + + @classmethod + def is_before(cls, old_time: Optional[DateTime], this_time: Optional[DateTime]) -> bool: + """ Check whether this time is before old time """ + if old_time is not None and this_time is not None: + return this_time.before(old_time) + + @classmethod + def is_expired(cls, this_doc: Document, old_doc: Document) -> bool: + """ Check whether this document's time is before old document's time """ + return cls.is_before(old_time=old_doc.time, this_time=this_doc.time) + + @classmethod + def last_document(cls, documents: List[Document], doc_type: str = None) -> Optional[Document]: + """ Select last document matched the type """ + if doc_type is None or doc_type == '*': + doc_type = '' + check_type = len(doc_type) > 0 + last: Optional[Document] = None + for item in documents: + # 1. check type + if check_type: + item_type = item.type + if item_type is not None and len(item_type) > 0 and item_type != doc_type: + # type not matched, skip it + continue + # 2. check time + if last is not None: + if cls.is_expired(this_doc=item, old_doc=last): + # skip expired document + continue + # got it + last = item + return last + + @classmethod + def last_visa(cls, documents: List[Document]) -> Optional[Visa]: + """ Select last visa document """ + last: Optional[Visa] = None + for item in documents: + # 1. check type + if not isinstance(item, Visa): + # type not matched, skip it + continue + # 2. check time + if last is not None and cls.is_expired(this_doc=item, old_doc=last): + # skip expired document + continue + # got it + last = item + return last + + @classmethod + def last_bulletin(cls, documents: List[Document]) -> Optional[Bulletin]: + """ Select last bulletin document """ + last: Optional[Bulletin] = None + for item in documents: + # 1. check type + if not isinstance(item, Bulletin): + # type not matched, skip it + continue + # 2. check time + if last is not None and cls.is_expired(this_doc=item, old_doc=last): + # skip expired document + continue + # got it + last = item + return last diff --git a/dimp/mkm/user.py b/dimp/mkm/user.py index b8fa3de..aa6fa4c 100644 --- a/dimp/mkm/user.py +++ b/dimp/mkm/user.py @@ -32,9 +32,12 @@ from typing import Optional, List from mkm.crypto import EncryptKey, DecryptKey, SignKey, VerifyKey -from mkm import ID, Visa +from mkm import ID -from .entity import EntityDataSource, Entity +from ..protocol import Visa + +from .helper import DocumentHelper +from .entity import EntityDataSource, Entity, BaseEntity class UserDataSource(EntityDataSource, ABC): @@ -228,3 +231,99 @@ def verify_visa(self, visa: Visa) -> bool: # NOTICE: only verify visa with meta.key # (if meta not exists, user won't be created) raise NotImplemented + + +class BaseUser(BaseEntity, User): + + # def __init__(self, identifier: ID): + # super().__init__(identifier=identifier) + + @BaseEntity.data_source.getter # Override + def data_source(self) -> Optional[UserDataSource]: + return super().data_source + + # @data_source.setter # Override + # def data_source(self, barrack: UserDataSource): + # super(BaseUser, BaseUser).data_source.__set__(self, barrack) + + @property # Override + def visa(self) -> Optional[Visa]: + return DocumentHelper.last_visa(documents=self.documents) + + @property # Override + def contacts(self) -> List[ID]: + barrack = self.data_source + # assert barrack is not None, 'user delegate not set yet' + return barrack.contacts(identifier=self.identifier) + + # Override + def verify(self, data: bytes, signature: bytes) -> bool: + barrack = self.data_source + assert barrack is not None, 'user data source not set yet' + keys = barrack.public_keys_for_verification(identifier=self.identifier) + assert len(keys) > 0, 'failed to get verify keys: %s' % self.identifier + for key in keys: + if key.verify(data=data, signature=signature): + # matched! + return True + # signature not match + # TODO: check whether visa is expired, query new document for this contact + + # Override + def encrypt(self, data: bytes) -> bytes: + barrack = self.data_source + assert isinstance(barrack, UserDataSource), 'user data source error: %s' % barrack + # NOTICE: meta.key will never changed, so use visa.key to encrypt message + # is the better way + key = barrack.public_key_for_encryption(identifier=self.identifier) + assert key is not None, 'failed to get encrypt key for user: %s' % self.identifier + return key.encrypt(data=data, extra={}) + + # Override + def sign(self, data: bytes) -> bytes: + barrack = self.data_source + assert barrack is not None, 'user data source not set yet' + key = barrack.private_key_for_signature(identifier=self.identifier) + assert key is not None, 'failed to get sign key for user: %s' % self.identifier + return key.sign(data=data) + + # Override + def decrypt(self, data: bytes) -> Optional[bytes]: + barrack = self.data_source + assert isinstance(barrack, UserDataSource), 'user data source error: %s' % barrack + # NOTICE: if you provide a public key in visa document for encryption, + # here you should return the private key paired with visa.key + keys = barrack.private_keys_for_decryption(identifier=self.identifier) + assert len(keys) > 0, 'failed to get decrypt keys: %s' % self.identifier + for key in keys: + # try decrypting it with each private key + plaintext = key.decrypt(data=data, params={}) + if plaintext is not None: + # OK! + return plaintext + # decryption failed + # TODO: check whether my visa key is changed, push new visa to this contact + + # Override + def sign_visa(self, visa: Visa) -> Optional[Visa]: + assert self.identifier == visa.identifier, 'visa ID not match: %s, %s' % (self.identifier, visa) + barrack = self.data_source + assert barrack is not None, 'user data source not set yet' + # NOTICE: only sign visa with the private key paired with your meta.key + key = barrack.private_key_for_visa_signature(identifier=self.identifier) + assert key is not None, 'failed to get sign key for visa: %s' % self.identifier + if visa.sign(private_key=key) is None: + assert False, 'failed to sign visa: %s, %s' % (self.identifier, visa) + else: + return visa + + # Override + def verify_visa(self, visa: Visa) -> bool: + # NOTICE: only verify visa with meta.key + # (if meta not exists, user won't be created) + if self.identifier != visa.identifier: + # visa ID not match + return False + key = self.meta.public_key + assert key is not None, 'failed to get meta key for visa: %s' % self.identifier + return visa.verify(public_key=key) diff --git a/dimp/mkm/user_impl.py b/dimp/mkm/user_impl.py deleted file mode 100644 index 1e8c22a..0000000 --- a/dimp/mkm/user_impl.py +++ /dev/null @@ -1,135 +0,0 @@ -# -*- coding: utf-8 -*- -# -# DIMP : Decentralized Instant Messaging Protocol -# -# Written in 2019 by Moky -# -# ============================================================================== -# MIT License -# -# Copyright (c) 2019 Albert Moky -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -# ============================================================================== - -from typing import Optional, List - -from mkm import ID, Visa - -from .user import User, UserDataSource -from .entity_impl import BaseEntity - - -class BaseUser(BaseEntity, User): - - # def __init__(self, identifier: ID): - # super().__init__(identifier=identifier) - - @BaseEntity.data_source.getter # Override - def data_source(self) -> Optional[UserDataSource]: - return super().data_source - - # @data_source.setter # Override - # def data_source(self, barrack: UserDataSource): - # super(BaseUser, BaseUser).data_source.__set__(self, barrack) - - @property # Override - def visa(self) -> Optional[Visa]: - doc = self.document() - if isinstance(doc, Visa): - return doc - assert doc is None, 'user document error: %s' % doc - - @property # Override - def contacts(self) -> List[ID]: - barrack = self.data_source - # assert barrack is not None, 'user delegate not set yet' - return barrack.contacts(identifier=self.identifier) - - # Override - def verify(self, data: bytes, signature: bytes) -> bool: - barrack = self.data_source - assert barrack is not None, 'user data source not set yet' - keys = barrack.public_keys_for_verification(identifier=self.identifier) - assert len(keys) > 0, 'failed to get verify keys: %s' % self.identifier - for key in keys: - if key.verify(data=data, signature=signature): - # matched! - return True - # signature not match - # TODO: check whether visa is expired, query new document for this contact - - # Override - def encrypt(self, data: bytes) -> bytes: - barrack = self.data_source - assert isinstance(barrack, UserDataSource), 'user data source error: %s' % barrack - # NOTICE: meta.key will never changed, so use visa.key to encrypt message - # is the better way - key = barrack.public_key_for_encryption(identifier=self.identifier) - assert key is not None, 'failed to get encrypt key for user: %s' % self.identifier - return key.encrypt(data=data, extra={}) - - # Override - def sign(self, data: bytes) -> bytes: - barrack = self.data_source - assert barrack is not None, 'user data source not set yet' - key = barrack.private_key_for_signature(identifier=self.identifier) - assert key is not None, 'failed to get sign key for user: %s' % self.identifier - return key.sign(data=data) - - # Override - def decrypt(self, data: bytes) -> Optional[bytes]: - barrack = self.data_source - assert isinstance(barrack, UserDataSource), 'user data source error: %s' % barrack - # NOTICE: if you provide a public key in visa document for encryption, - # here you should return the private key paired with visa.key - keys = barrack.private_keys_for_decryption(identifier=self.identifier) - assert len(keys) > 0, 'failed to get decrypt keys: %s' % self.identifier - for key in keys: - # try decrypting it with each private key - plaintext = key.decrypt(data=data, params={}) - if plaintext is not None: - # OK! - return plaintext - # decryption failed - # TODO: check whether my visa key is changed, push new visa to this contact - - # Override - def sign_visa(self, visa: Visa) -> Optional[Visa]: - assert self.identifier == visa.identifier, 'visa ID not match: %s, %s' % (self.identifier, visa) - barrack = self.data_source - assert barrack is not None, 'user data source not set yet' - # NOTICE: only sign visa with the private key paired with your meta.key - key = barrack.private_key_for_visa_signature(identifier=self.identifier) - assert key is not None, 'failed to get sign key for visa: %s' % self.identifier - if visa.sign(private_key=key) is None: - assert False, 'failed to sign visa: %s, %s' % (self.identifier, visa) - else: - return visa - - # Override - def verify_visa(self, visa: Visa) -> bool: - # NOTICE: only verify visa with meta.key - # (if meta not exists, user won't be created) - if self.identifier != visa.identifier: - # visa ID not match - return False - key = self.meta.public_key - assert key is not None, 'failed to get meta key for visa: %s' % self.identifier - return visa.verify(public_key=key) diff --git a/dimp/msg/base.py b/dimp/msg/base.py index 2f786bf..f708594 100644 --- a/dimp/msg/base.py +++ b/dimp/msg/base.py @@ -72,7 +72,6 @@ def __init__(self, msg: Dict[str, Any] = None, head: Envelope = None): super().__init__(dictionary=msg) # lazy self.__envelope = head - self.__delegate = None @property # Override def envelope(self) -> Envelope: diff --git a/dimp/msg/envelope.py b/dimp/msg/envelope.py index e93a0fe..60d1ba0 100644 --- a/dimp/msg/envelope.py +++ b/dimp/msg/envelope.py @@ -28,13 +28,12 @@ # SOFTWARE. # ============================================================================== -from typing import Optional, Union, Any, Dict +from typing import Optional, Any, Dict from mkm.types import DateTime from mkm.types import Dictionary from mkm import ID, ANYONE -from dkd import ContentType from dkd import Envelope @@ -78,8 +77,6 @@ def __init__(self, envelope: Dict[str, Any] = None, self.__sender = sender self.__receiver = receiver self.__time = time - self.__group = None - self.__type = None @property # Override def sender(self) -> ID: @@ -92,42 +89,32 @@ def sender(self) -> ID: def receiver(self) -> ID: if self.__receiver is None: identifier = self.get('receiver') + identifier = ID.parse(identifier=identifier) if identifier is None: self.__receiver = ANYONE else: - self.__receiver = ID.parse(identifier=identifier) + self.__receiver = identifier return self.__receiver @property # Override - def time(self) -> DateTime: + def time(self) -> Optional[DateTime]: if self.__time is None: self.__time = self.get_datetime(key='time', default=None) return self.__time @property # Override def group(self) -> Optional[ID]: - if self.__group is None: - identifier = self.get('group') - self.__group = ID.parse(identifier=identifier) - return self.__group + identifier = self.get('group') + return ID.parse(identifier=identifier) @group.setter # Override def group(self, value: ID): self.set_string(key='group', value=value) - self.__group = value @property # Override def type(self) -> Optional[int]: - if self.__type is None: - self.__type = self.get_int(key='type', default=None) - return self.__type + return self.get_int(key='type', default=None) @type.setter # Override - def type(self, value: Union[int, ContentType]): - if isinstance(value, ContentType): - value = value.value - if value is None: # or value == 0: - self.pop('type', None) - else: - self['type'] = value - self.__type = value + def type(self, value: int): + self['type'] = value diff --git a/dimp/msg/instant.py b/dimp/msg/instant.py index 6bfda9f..0aa682d 100644 --- a/dimp/msg/instant.py +++ b/dimp/msg/instant.py @@ -75,8 +75,9 @@ def __init__(self, msg: Dict[str, Any] = None, def content(self) -> Content: if self.__content is None: content = self.get('content') + content = Content.parse(content=content) assert content is not None, 'message content not found: %s' % self - self.__content = Content.parse(content=content) + self.__content = content return self.__content @content.setter # Override diff --git a/dimp/msg/reliable.py b/dimp/msg/reliable.py index 3656d7c..3d6cfe7 100644 --- a/dimp/msg/reliable.py +++ b/dimp/msg/reliable.py @@ -31,7 +31,6 @@ from typing import Optional, Any, Dict from mkm.format import TransportableData -from mkm import Meta, Document, Visa from dkd import ReliableMessage @@ -67,8 +66,6 @@ def __init__(self, msg: Dict[str, Any]): super().__init__(msg=msg) # lazy self.__signature: Optional[TransportableData] = None - self.__meta: Optional[Meta] = None - self.__visa: Optional[Visa] = None @property # Override def signature(self) -> bytes: @@ -80,29 +77,3 @@ def signature(self) -> bytes: assert ted is not None, 'failed to decode message signature: %s' % base64 if ted is not None: return ted.data - - @property # Override - def meta(self) -> Optional[Meta]: - if self.__meta is None: - self.__meta = Meta.parse(meta=self.get('meta')) - return self.__meta - - @meta.setter # Override - def meta(self, info: Meta): - self.set_map(key='meta', value=info) - self.__meta = info - - @property # Override - def visa(self) -> Optional[Visa]: - if self.__visa is None: - doc = Document.parse(document=self.get('visa')) - if isinstance(doc, Visa): - self.__visa = doc - else: - assert doc is None, 'visa document error: %s' % doc - return self.__visa - - @visa.setter # Override - def visa(self, info: Visa): - self.set_map(key='visa', value=info) - self.__visa = info diff --git a/dimp/protocol/__init__.py b/dimp/protocol/__init__.py index 03c065e..620552e 100644 --- a/dimp/protocol/__init__.py +++ b/dimp/protocol/__init__.py @@ -48,6 +48,8 @@ from mkm import ANYWHERE, EVERYWHERE, ANYONE, EVERYONE, FOUNDER # from dkd import InstantMessageDelegate, SecureMessageDelegate, ReliableMessageDelegate +from .docs import Visa, Bulletin + from .contents import TextContent, ArrayContent, ForwardContent from .contents import PageContent, NameCard from .files import FileContent, ImageContent, AudioContent, VideoContent @@ -62,6 +64,7 @@ from .groups import InviteCommand, ExpelCommand, JoinCommand, QuitCommand, QueryCommand, ResetCommand from .groups import HireCommand, FireCommand, ResignCommand + __all__ = [ 'URI', 'DateTime', diff --git a/dimp/protocol/docs.py b/dimp/protocol/docs.py new file mode 100644 index 0000000..79f7c1f --- /dev/null +++ b/dimp/protocol/docs.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +# +# Ming-Ke-Ming : Decentralized User Identity Authentication +# +# Written in 2019 by Moky +# +# ============================================================================== +# MIT License +# +# Copyright (c) 2019 Albert Moky +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================== + +from abc import ABC, abstractmethod +from typing import Optional, List + +from mkm.crypto import EncryptKey +from mkm.format import PortableNetworkFile + +from mkm import ID +from mkm import Document + + +class Visa(Document, ABC): + """ + User Document + ~~~~~~~~~~~~~ + This interface is defined for authorizing other apps to login, + which can generate a temporary asymmetric key pair for messaging. + """ + + @property + @abstractmethod + def public_key(self) -> Optional[EncryptKey]: + """ + Get public key to encrypt message for user + + :return: public key + """ + raise NotImplemented + + @public_key.setter + @abstractmethod + def public_key(self, key: EncryptKey): + """ + Set public key for other user to encrypt message + + :param key: public key as visa.key + """ + raise NotImplemented + + @property + @abstractmethod + def avatar(self) -> Optional[PortableNetworkFile]: + """ + Get avatar URL + + :return: PNF(URL) + """ + raise NotImplemented + + @avatar.setter + @abstractmethod + def avatar(self, url: PortableNetworkFile): + """ + Set avatar URL + + :param url: PNF(URL) + """ + raise NotImplemented + + +class Bulletin(Document, ABC): + """ + Group Document + ~~~~~~~~~~~~~~ + """ + + @property + @abstractmethod + def founder(self) -> Optional[ID]: + """ + Get group founder + + :return: user ID + """ + raise NotImplemented + + @property + @abstractmethod + def assistants(self) -> Optional[List[ID]]: + """ + Get group assistants + + :return: bot ID list + """ + raise NotImplemented + + @assistants.setter + @abstractmethod + def assistants(self, bots: List[ID]): + """ + Set group assistants + + :param bots: bot ID list + """ + raise NotImplemented diff --git a/dimp/protocol/files.py b/dimp/protocol/files.py index 41cbd28..d5ec284 100644 --- a/dimp/protocol/files.py +++ b/dimp/protocol/files.py @@ -29,7 +29,7 @@ # ============================================================================== from abc import ABC, abstractmethod -from typing import Optional, Union +from typing import Optional from mkm.types import URI from mkm.format import TransportableData @@ -107,15 +107,10 @@ def password(self, key: DecryptKey): # @classmethod - def create(cls, msg_type: Union[int, ContentType] = None, + def create(cls, msg_type: int, data: Optional[TransportableData] = None, filename: Optional[str] = None, url: Optional[URI] = None, password: Optional[DecryptKey] = None): - # convert type value - if msg_type is None: - msg_type = ContentType.FILE.value - elif isinstance(msg_type, ContentType): - msg_type = msg_type.value - # check type value + assert msg_type > 0, 'file type error: %d' % msg_type if msg_type == ContentType.IMAGE.value: from ..dkd import ImageFileContent return ImageFileContent(data=data, filename=filename, url=url, password=password) diff --git a/dimp/protocol/money.py b/dimp/protocol/money.py index b33e1ef..1875d85 100644 --- a/dimp/protocol/money.py +++ b/dimp/protocol/money.py @@ -29,7 +29,7 @@ # ============================================================================== from abc import ABC, abstractmethod -from typing import Union, Optional +from typing import Optional from mkm import ID from dkd import ContentType @@ -69,12 +69,10 @@ def amount(self, value: float): # Factory method # @classmethod - def create(cls, currency: str, amount: float, msg_type: Union[int, ContentType] = None): + def create(cls, currency: str, amount: float, msg_type: int = None): # convert type value if msg_type is None: msg_type = ContentType.MONEY.value - elif isinstance(msg_type, ContentType): - msg_type = msg_type.value # create with type value from ..dkd import BaseMoneyContent return BaseMoneyContent(msg_type=msg_type, currency=currency, amount=amount) diff --git a/setup.py b/setup.py index 0b370f3..80f9892 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ from setuptools import setup, find_packages -__version__ = '1.0.1' +__version__ = '1.0.2' __author__ = 'Albert Moky' __contact__ = 'albert.moky@gmail.com' @@ -38,7 +38,7 @@ 'Operating System :: OS Independent', ], install_requires=[ - 'dkd>=1.0.1', - 'mkm>=1.0.1', + 'dkd>=1.0.2', + 'mkm>=1.0.2', ] )