Skip to content

Commit

Permalink
dimp 1.0.2: define Visa & Bulletin
Browse files Browse the repository at this point in the history
DocumentHelper;
BroadcastHelper
  • Loading branch information
moky committed Oct 29, 2023
1 parent 66c05df commit 86344f5
Show file tree
Hide file tree
Showing 29 changed files with 736 additions and 574 deletions.
3 changes: 3 additions & 0 deletions dimp/__init__.py
Expand Up @@ -88,6 +88,8 @@
'User', 'UserDataSource', 'BaseUser',
'Group', 'GroupDataSource', 'BaseGroup',

'DocumentHelper', 'BroadcastHelper', # 'thanos',

#
# DaoKeDao
#
Expand Down Expand Up @@ -141,4 +143,5 @@
# Core
#
'Barrack', 'Transceiver', 'Packer', 'Processor',

]
97 changes: 27 additions & 70 deletions dimp/barrack.py
Expand Up @@ -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
Expand Down Expand Up @@ -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
#
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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
4 changes: 4 additions & 0 deletions dimp/crypto/keys.py
Expand Up @@ -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
Expand All @@ -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
Expand Down
11 changes: 6 additions & 5 deletions dimp/crypto/pnf.py
Expand Up @@ -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]):
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down
8 changes: 8 additions & 0 deletions dimp/crypto/ted.py
Expand Up @@ -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='')
Expand Down
2 changes: 1 addition & 1 deletion dimp/dkd/__init__.py
Expand Up @@ -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

Expand Down
6 changes: 5 additions & 1 deletion dimp/dkd/contents.py
Expand Up @@ -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
3 changes: 2 additions & 1 deletion dimp/dkd/factory.py
Expand Up @@ -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:
Expand Down
106 changes: 106 additions & 0 deletions dimp/dkd/group_admins.py
@@ -0,0 +1,106 @@
# -*- coding: utf-8 -*-
#
# DIMP : Decentralized Instant Messaging Protocol
#
# Written in 2023 by Moky <albert.moky@gmail.com>
#
# ==============================================================================
# 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)

0 comments on commit 86344f5

Please sign in to comment.