dimp 2.3.2__tar.gz → 2.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {dimp-2.3.2 → dimp-2.3.3}/PKG-INFO +68 -47
  2. {dimp-2.3.2 → dimp-2.3.3}/README.md +67 -46
  3. {dimp-2.3.2 → dimp-2.3.3}/dimp/__init__.py +10 -8
  4. {dimp-2.3.2 → dimp-2.3.3}/dimp/crypto/keys.py +1 -1
  5. {dimp-2.3.2 → dimp-2.3.3}/dimp/crypto/pnf.py +2 -2
  6. {dimp-2.3.2 → dimp-2.3.3}/dimp/crypto/ted.py +5 -5
  7. {dimp-2.3.2 → dimp-2.3.3}/dimp/dkd/__init__.py +9 -6
  8. {dimp-2.3.2 → dimp-2.3.3}/dimp/mkm/document.py +3 -3
  9. {dimp-2.3.2 → dimp-2.3.3}/dimp/msg/envelope.py +2 -2
  10. {dimp-2.3.2 → dimp-2.3.3}/dimp/protocol/__init__.py +2 -0
  11. {dimp-2.3.2 → dimp-2.3.3}/dimp/protocol/base.py +1 -1
  12. {dimp-2.3.2 → dimp-2.3.3}/dimp/protocol/commands.py +1 -1
  13. {dimp-2.3.2 → dimp-2.3.3}/dimp/protocol/contents.py +4 -4
  14. dimp-2.3.3/dimp/protocol/customized.py +113 -0
  15. {dimp-2.3.2 → dimp-2.3.3}/dimp/protocol/files.py +3 -3
  16. {dimp-2.3.2 → dimp-2.3.3}/dimp/protocol/groups.py +1 -1
  17. {dimp-2.3.2 → dimp-2.3.3}/dimp.egg-info/PKG-INFO +68 -47
  18. {dimp-2.3.2 → dimp-2.3.3}/dimp.egg-info/SOURCES.txt +1 -0
  19. dimp-2.3.3/dimp.egg-info/requires.txt +2 -0
  20. {dimp-2.3.2 → dimp-2.3.3}/setup.py +3 -3
  21. dimp-2.3.2/dimp.egg-info/requires.txt +0 -2
  22. {dimp-2.3.2 → dimp-2.3.3}/LICENSE +0 -0
  23. {dimp-2.3.2 → dimp-2.3.3}/dimp/crypto/__init__.py +0 -0
  24. {dimp-2.3.2 → dimp-2.3.3}/dimp/crypto/algorithms.py +0 -0
  25. {dimp-2.3.2 → dimp-2.3.3}/dimp/mkm/__init__.py +0 -0
  26. {dimp-2.3.2 → dimp-2.3.3}/dimp/mkm/docs.py +0 -0
  27. {dimp-2.3.2 → dimp-2.3.3}/dimp/mkm/meta.py +0 -0
  28. {dimp-2.3.2 → dimp-2.3.3}/dimp/msg/__init__.py +0 -0
  29. {dimp-2.3.2 → dimp-2.3.3}/dimp/msg/base.py +0 -0
  30. {dimp-2.3.2 → dimp-2.3.3}/dimp/msg/instant.py +0 -0
  31. {dimp-2.3.2 → dimp-2.3.3}/dimp/msg/reliable.py +0 -0
  32. {dimp-2.3.2 → dimp-2.3.3}/dimp/msg/secure.py +0 -0
  33. {dimp-2.3.2 → dimp-2.3.3}/dimp/plugins/__init__.py +0 -0
  34. {dimp-2.3.2 → dimp-2.3.3}/dimp/protocol/docs.py +0 -0
  35. {dimp-2.3.2 → dimp-2.3.3}/dimp/protocol/forward.py +0 -0
  36. {dimp-2.3.2 → dimp-2.3.3}/dimp/protocol/helpers.py +0 -0
  37. {dimp-2.3.2 → dimp-2.3.3}/dimp/protocol/money.py +0 -0
  38. {dimp-2.3.2 → dimp-2.3.3}/dimp/protocol/quote.py +0 -0
  39. {dimp-2.3.2 → dimp-2.3.3}/dimp/protocol/receipt.py +0 -0
  40. {dimp-2.3.2 → dimp-2.3.3}/dimp/protocol/types.py +0 -0
  41. {dimp-2.3.2 → dimp-2.3.3}/dimp.egg-info/dependency_links.txt +0 -0
  42. {dimp-2.3.2 → dimp-2.3.3}/dimp.egg-info/top_level.txt +0 -0
  43. {dimp-2.3.2 → dimp-2.3.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dimp
3
- Version: 2.3.2
3
+ Version: 2.3.3
4
4
  Summary: Decentralized Instant Messaging Protocol
5
5
  Home-page: https://github.com/dimchat/core-py
6
6
  Author: Albert Moky
@@ -45,8 +45,9 @@ License-File: LICENSE
45
45
  3. (S-C) handshake success
46
46
 
47
47
  ```python
48
+ from abc import ABC, abstractmethod
48
49
  from enum import IntEnum
49
- from typing import Optional, Any, Dict
50
+ from typing import Optional, Dict
50
51
 
51
52
  from dimp import Command, BaseCommand
52
53
 
@@ -58,7 +59,20 @@ class HandshakeState(IntEnum):
58
59
  Success = 3 # S -> C, handshake accepted
59
60
 
60
61
 
61
- class HandshakeCommand(BaseCommand):
62
+ def handshake_state(title: str, session: str = None) -> HandshakeState:
63
+ # Server -> Client
64
+ if title == 'DIM!': # or title == 'OK!':
65
+ return HandshakeState.Success
66
+ if title == 'DIM?':
67
+ return HandshakeState.Again
68
+ # Client -> Server: "Hello world!"
69
+ if session is None or len(session) == 0:
70
+ return HandshakeState.Start
71
+ else:
72
+ return HandshakeState.Restart
73
+
74
+
75
+ class HandshakeCommand(Command, ABC):
62
76
  """
63
77
  Handshake Command
64
78
  ~~~~~~~~~~~~~~~~~
@@ -74,37 +88,24 @@ class HandshakeCommand(BaseCommand):
74
88
  """
75
89
  HANDSHAKE = 'handshake'
76
90
 
77
- def __init__(self, content: Dict[str, Any] = None, title: str = None, session: str = None):
78
- if content is None:
79
- # 1. new command with title & session key
80
- assert title is not None, 'handshake command error: %s' % session
81
- cmd = self.HANDSHAKE
82
- super().__init__(cmd=cmd)
83
- self['title'] = title
84
- self['message'] = title # TODO: remove after all clients upgraded
85
- if session is not None:
86
- self['session'] = session
87
- else:
88
- # 2. command info from network
89
- assert title is None and session is None, 'params error: %s, %s, %s' % (content, title, session)
90
- super().__init__(content)
91
-
92
91
  @property
92
+ @abstractmethod
93
93
  def title(self) -> str:
94
- # TODO: modify after all clients upgraded
95
- text = self.get_str(key='title', default=None)
96
- if text is None:
97
- # compatible with v1.0
98
- text = self.get_str(key='message', default='')
99
- return text
94
+ raise NotImplemented
100
95
 
101
96
  @property
97
+ @abstractmethod
102
98
  def session(self) -> Optional[str]:
103
- return self.get_str(key='session', default=None)
99
+ raise NotImplemented
104
100
 
105
101
  @property
102
+ @abstractmethod
106
103
  def state(self) -> HandshakeState:
107
- return handshake_state(title=self.title, session=self.session)
104
+ raise NotImplemented
105
+
106
+ #
107
+ # Factories
108
+ #
108
109
 
109
110
  @classmethod
110
111
  def offer(cls, session: str = None) -> Command:
@@ -114,7 +115,7 @@ class HandshakeCommand(BaseCommand):
114
115
  :param session: Old session key
115
116
  :return: HandshakeCommand object
116
117
  """
117
- return HandshakeCommand(title='Hello world!', session=session)
118
+ return BaseHandshakeCommand(title='Hello world!', session=session)
118
119
 
119
120
  @classmethod
120
121
  def ask(cls, session: str) -> Command:
@@ -124,7 +125,7 @@ class HandshakeCommand(BaseCommand):
124
125
  :param session: New session key
125
126
  :return: HandshakeCommand object
126
127
  """
127
- return HandshakeCommand(title='DIM?', session=session)
128
+ return BaseHandshakeCommand(title='DIM?', session=session)
128
129
 
129
130
  @classmethod
130
131
  def accepted(cls, session: str = None) -> Command:
@@ -133,7 +134,7 @@ class HandshakeCommand(BaseCommand):
133
134
 
134
135
  :return: HandshakeCommand object
135
136
  """
136
- return HandshakeCommand(title='DIM!', session=session)
137
+ return BaseHandshakeCommand(title='DIM!', session=session)
137
138
 
138
139
  start = offer # (1. C->S) first handshake, without session
139
140
  again = ask # (2. S->C) ask client to handshake with new session key
@@ -141,17 +142,34 @@ class HandshakeCommand(BaseCommand):
141
142
  success = accepted # (4. S->C) notice the client that handshake accepted
142
143
 
143
144
 
144
- def handshake_state(title: str, session: str = None) -> HandshakeState:
145
- # Server -> Client
146
- if title == 'DIM!': # or title == 'OK!':
147
- return HandshakeState.Success
148
- if title == 'DIM?':
149
- return HandshakeState.Again
150
- # Client -> Server: "Hello world!"
151
- if session is None or len(session) == 0:
152
- return HandshakeState.Start
153
- else:
154
- return HandshakeState.Restart
145
+ class BaseHandshakeCommand(BaseCommand, HandshakeCommand):
146
+
147
+ def __init__(self, content: Dict = None, title: str = None, session: str = None):
148
+ if content is None:
149
+ # 1. new command with title & session key
150
+ assert title is not None, 'handshake command error: %s' % session
151
+ cmd = self.HANDSHAKE
152
+ super().__init__(cmd=cmd)
153
+ self['title'] = title
154
+ self['message'] = title # TODO: remove after all clients upgraded
155
+ if session is not None:
156
+ self['session'] = session
157
+ else:
158
+ # 2. command info from network
159
+ assert title is None and session is None, 'params error: %s, %s, %s' % (content, title, session)
160
+ super().__init__(content)
161
+
162
+ @property
163
+ def title(self) -> str:
164
+ return self.get_str(key='title', default='')
165
+
166
+ @property
167
+ def session(self) -> Optional[str]:
168
+ return self.get_str(key='session')
169
+
170
+ @property
171
+ def state(self) -> HandshakeState:
172
+ return handshake_state(title=self.title, session=self.session)
155
173
  ```
156
174
 
157
175
  ### extends Content
@@ -165,13 +183,13 @@ from dimp import Content
165
183
  from dimp import BaseContent
166
184
 
167
185
 
168
- class CustomizedContent(Content, ABC):
186
+ class AppContent(Content, ABC):
169
187
  """
170
188
  Application Customized message
171
189
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
172
190
 
173
191
  data format: {
174
- type : 0xCC,
192
+ type : 0xA0,
175
193
  sn : 123,
176
194
 
177
195
  app : "{APP_ID}", // application (e.g.: "chat.dim.sechat")
@@ -199,22 +217,25 @@ class CustomizedContent(Content, ABC):
199
217
  """ Action Name """
200
218
  raise NotImplemented
201
219
 
220
+ #
221
+ # Factory
222
+ #
202
223
  @classmethod
203
224
  def create(cls, app: str, mod: str, act: str):
204
- return AppCustomizedContent(app=app, mod=mod, act=act)
225
+ return ApplicationContent(app=app, mod=mod, act=act)
205
226
 
206
227
 
207
- class AppCustomizedContent(BaseContent, CustomizedContent):
228
+ class ApplicationContent(BaseContent, AppContent):
208
229
 
209
230
  def __init__(self, content: Dict[str, Any] = None,
210
- msg_type: int = None,
231
+ msg_type: str = None,
211
232
  app: str = None, mod: str = None, act: str = None):
212
233
  if content is None:
213
234
  # 1. new content with type, application, module & action
214
235
  assert app is not None and mod is not None and act is not None, \
215
236
  'customized content error: %s, %s, %s, %s' % (msg_type, app, mod, act)
216
237
  if msg_type is None:
217
- msg_type = ContentType.CUSTOMIZED.value
238
+ msg_type = ContentType.APPLICATION
218
239
  super().__init__(None, msg_type)
219
240
  self['app'] = app
220
241
  self['mod'] = mod
@@ -244,5 +265,5 @@ class AppCustomizedContent(BaseContent, CustomizedContent):
244
265
 
245
266
  ----
246
267
 
247
- Copyright © 2018 Albert Moky
268
+ Copyright © 2018-2025 Albert Moky
248
269
  [![Followers](https://img.shields.io/github/followers/moky)](https://github.com/moky?tab=followers)
@@ -31,8 +31,9 @@
31
31
  3. (S-C) handshake success
32
32
 
33
33
  ```python
34
+ from abc import ABC, abstractmethod
34
35
  from enum import IntEnum
35
- from typing import Optional, Any, Dict
36
+ from typing import Optional, Dict
36
37
 
37
38
  from dimp import Command, BaseCommand
38
39
 
@@ -44,7 +45,20 @@ class HandshakeState(IntEnum):
44
45
  Success = 3 # S -> C, handshake accepted
45
46
 
46
47
 
47
- class HandshakeCommand(BaseCommand):
48
+ def handshake_state(title: str, session: str = None) -> HandshakeState:
49
+ # Server -> Client
50
+ if title == 'DIM!': # or title == 'OK!':
51
+ return HandshakeState.Success
52
+ if title == 'DIM?':
53
+ return HandshakeState.Again
54
+ # Client -> Server: "Hello world!"
55
+ if session is None or len(session) == 0:
56
+ return HandshakeState.Start
57
+ else:
58
+ return HandshakeState.Restart
59
+
60
+
61
+ class HandshakeCommand(Command, ABC):
48
62
  """
49
63
  Handshake Command
50
64
  ~~~~~~~~~~~~~~~~~
@@ -60,37 +74,24 @@ class HandshakeCommand(BaseCommand):
60
74
  """
61
75
  HANDSHAKE = 'handshake'
62
76
 
63
- def __init__(self, content: Dict[str, Any] = None, title: str = None, session: str = None):
64
- if content is None:
65
- # 1. new command with title & session key
66
- assert title is not None, 'handshake command error: %s' % session
67
- cmd = self.HANDSHAKE
68
- super().__init__(cmd=cmd)
69
- self['title'] = title
70
- self['message'] = title # TODO: remove after all clients upgraded
71
- if session is not None:
72
- self['session'] = session
73
- else:
74
- # 2. command info from network
75
- assert title is None and session is None, 'params error: %s, %s, %s' % (content, title, session)
76
- super().__init__(content)
77
-
78
77
  @property
78
+ @abstractmethod
79
79
  def title(self) -> str:
80
- # TODO: modify after all clients upgraded
81
- text = self.get_str(key='title', default=None)
82
- if text is None:
83
- # compatible with v1.0
84
- text = self.get_str(key='message', default='')
85
- return text
80
+ raise NotImplemented
86
81
 
87
82
  @property
83
+ @abstractmethod
88
84
  def session(self) -> Optional[str]:
89
- return self.get_str(key='session', default=None)
85
+ raise NotImplemented
90
86
 
91
87
  @property
88
+ @abstractmethod
92
89
  def state(self) -> HandshakeState:
93
- return handshake_state(title=self.title, session=self.session)
90
+ raise NotImplemented
91
+
92
+ #
93
+ # Factories
94
+ #
94
95
 
95
96
  @classmethod
96
97
  def offer(cls, session: str = None) -> Command:
@@ -100,7 +101,7 @@ class HandshakeCommand(BaseCommand):
100
101
  :param session: Old session key
101
102
  :return: HandshakeCommand object
102
103
  """
103
- return HandshakeCommand(title='Hello world!', session=session)
104
+ return BaseHandshakeCommand(title='Hello world!', session=session)
104
105
 
105
106
  @classmethod
106
107
  def ask(cls, session: str) -> Command:
@@ -110,7 +111,7 @@ class HandshakeCommand(BaseCommand):
110
111
  :param session: New session key
111
112
  :return: HandshakeCommand object
112
113
  """
113
- return HandshakeCommand(title='DIM?', session=session)
114
+ return BaseHandshakeCommand(title='DIM?', session=session)
114
115
 
115
116
  @classmethod
116
117
  def accepted(cls, session: str = None) -> Command:
@@ -119,7 +120,7 @@ class HandshakeCommand(BaseCommand):
119
120
 
120
121
  :return: HandshakeCommand object
121
122
  """
122
- return HandshakeCommand(title='DIM!', session=session)
123
+ return BaseHandshakeCommand(title='DIM!', session=session)
123
124
 
124
125
  start = offer # (1. C->S) first handshake, without session
125
126
  again = ask # (2. S->C) ask client to handshake with new session key
@@ -127,17 +128,34 @@ class HandshakeCommand(BaseCommand):
127
128
  success = accepted # (4. S->C) notice the client that handshake accepted
128
129
 
129
130
 
130
- def handshake_state(title: str, session: str = None) -> HandshakeState:
131
- # Server -> Client
132
- if title == 'DIM!': # or title == 'OK!':
133
- return HandshakeState.Success
134
- if title == 'DIM?':
135
- return HandshakeState.Again
136
- # Client -> Server: "Hello world!"
137
- if session is None or len(session) == 0:
138
- return HandshakeState.Start
139
- else:
140
- return HandshakeState.Restart
131
+ class BaseHandshakeCommand(BaseCommand, HandshakeCommand):
132
+
133
+ def __init__(self, content: Dict = None, title: str = None, session: str = None):
134
+ if content is None:
135
+ # 1. new command with title & session key
136
+ assert title is not None, 'handshake command error: %s' % session
137
+ cmd = self.HANDSHAKE
138
+ super().__init__(cmd=cmd)
139
+ self['title'] = title
140
+ self['message'] = title # TODO: remove after all clients upgraded
141
+ if session is not None:
142
+ self['session'] = session
143
+ else:
144
+ # 2. command info from network
145
+ assert title is None and session is None, 'params error: %s, %s, %s' % (content, title, session)
146
+ super().__init__(content)
147
+
148
+ @property
149
+ def title(self) -> str:
150
+ return self.get_str(key='title', default='')
151
+
152
+ @property
153
+ def session(self) -> Optional[str]:
154
+ return self.get_str(key='session')
155
+
156
+ @property
157
+ def state(self) -> HandshakeState:
158
+ return handshake_state(title=self.title, session=self.session)
141
159
  ```
142
160
 
143
161
  ### extends Content
@@ -151,13 +169,13 @@ from dimp import Content
151
169
  from dimp import BaseContent
152
170
 
153
171
 
154
- class CustomizedContent(Content, ABC):
172
+ class AppContent(Content, ABC):
155
173
  """
156
174
  Application Customized message
157
175
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
158
176
 
159
177
  data format: {
160
- type : 0xCC,
178
+ type : 0xA0,
161
179
  sn : 123,
162
180
 
163
181
  app : "{APP_ID}", // application (e.g.: "chat.dim.sechat")
@@ -185,22 +203,25 @@ class CustomizedContent(Content, ABC):
185
203
  """ Action Name """
186
204
  raise NotImplemented
187
205
 
206
+ #
207
+ # Factory
208
+ #
188
209
  @classmethod
189
210
  def create(cls, app: str, mod: str, act: str):
190
- return AppCustomizedContent(app=app, mod=mod, act=act)
211
+ return ApplicationContent(app=app, mod=mod, act=act)
191
212
 
192
213
 
193
- class AppCustomizedContent(BaseContent, CustomizedContent):
214
+ class ApplicationContent(BaseContent, AppContent):
194
215
 
195
216
  def __init__(self, content: Dict[str, Any] = None,
196
- msg_type: int = None,
217
+ msg_type: str = None,
197
218
  app: str = None, mod: str = None, act: str = None):
198
219
  if content is None:
199
220
  # 1. new content with type, application, module & action
200
221
  assert app is not None and mod is not None and act is not None, \
201
222
  'customized content error: %s, %s, %s, %s' % (msg_type, app, mod, act)
202
223
  if msg_type is None:
203
- msg_type = ContentType.CUSTOMIZED.value
224
+ msg_type = ContentType.APPLICATION
204
225
  super().__init__(None, msg_type)
205
226
  self['app'] = app
206
227
  self['mod'] = mod
@@ -230,5 +251,5 @@ class AppCustomizedContent(BaseContent, CustomizedContent):
230
251
 
231
252
  ----
232
253
 
233
- Copyright © 2018 Albert Moky
254
+ Copyright © 2018-2025 Albert Moky
234
255
  [![Followers](https://img.shields.io/github/followers/moky)](https://github.com/moky?tab=followers)
@@ -119,6 +119,8 @@ __all__ = [
119
119
  'Meta',
120
120
  'Document', 'Visa', 'Bulletin',
121
121
 
122
+ 'MetaType', 'DocumentType',
123
+
122
124
  'AddressFactory',
123
125
  'IDFactory',
124
126
  'MetaFactory',
@@ -132,8 +134,6 @@ __all__ = [
132
134
  'BaseMeta',
133
135
  'BaseDocument', 'BaseVisa', 'BaseBulletin',
134
136
 
135
- 'MetaType', 'DocumentType',
136
-
137
137
  #
138
138
  # DaoKeDao
139
139
  #
@@ -145,11 +145,12 @@ __all__ = [
145
145
  'InstantMessage', 'SecureMessage', 'ReliableMessage',
146
146
 
147
147
  # contents
148
- 'TextContent', 'ArrayContent', 'ForwardContent',
149
- 'PageContent', 'NameCard',
148
+ 'TextContent', 'PageContent', 'NameCard',
149
+ 'ForwardContent', 'CombineContent', 'ArrayContent',
150
150
  'FileContent', 'ImageContent', 'AudioContent', 'VideoContent',
151
151
  'MoneyContent', 'TransferContent',
152
- 'QuoteContent', 'CombineContent',
152
+ 'QuoteContent',
153
+ 'CustomizedContent',
153
154
 
154
155
  # commands
155
156
  'Command',
@@ -163,11 +164,12 @@ __all__ = [
163
164
 
164
165
  # extend contents
165
166
  'BaseContent',
166
- 'BaseTextContent', 'ListContent', 'SecretContent',
167
- 'WebPageContent', 'NameCardContent',
167
+ 'BaseTextContent', 'WebPageContent', 'NameCardContent',
168
+ 'SecretContent', 'CombineForwardContent', 'ListContent',
168
169
  'BaseFileContent', 'ImageFileContent', 'AudioFileContent', 'VideoFileContent',
169
170
  'BaseMoneyContent', 'TransferMoneyContent',
170
- 'BaseQuoteContent', 'CombineForwardContent',
171
+ 'BaseQuoteContent',
172
+ 'AppCustomizedContent',
171
173
 
172
174
  # extend commands
173
175
  'BaseCommand',
@@ -60,7 +60,7 @@ class BaseKey(Dictionary, CryptographyKey, ABC):
60
60
  @classmethod
61
61
  def get_key_algorithm(cls, key: Dict) -> str:
62
62
  ext = SharedCryptoExtensions()
63
- algorithm = ext.helper.get_key_algorithm(key=key, default=None)
63
+ algorithm = ext.helper.get_key_algorithm(key=key)
64
64
  return '' if algorithm is None else algorithm
65
65
 
66
66
  @classmethod
@@ -100,7 +100,7 @@ class BaseFileWrapper(Dictionary):
100
100
 
101
101
  @property
102
102
  def filename(self) -> Optional[str]:
103
- return self.get_str(key='filename', default=None)
103
+ return self.get_str(key='filename')
104
104
 
105
105
  @filename.setter
106
106
  def filename(self, name: Optional[str]):
@@ -117,7 +117,7 @@ class BaseFileWrapper(Dictionary):
117
117
  def url(self) -> Optional[URI]:
118
118
  remote = self.__url
119
119
  if remote is None:
120
- remote = self.get_str(key='URL', default=None)
120
+ remote = self.get_str(key='URL')
121
121
  # TODO: convert str to URI
122
122
  self.__url = remote
123
123
  return remote
@@ -69,10 +69,10 @@ class BaseDataWrapper(Dictionary):
69
69
 
70
70
  # Override
71
71
  def __str__(self) -> str:
72
- text = self.get_str(key='data', default=None)
72
+ text = self.get_str(key='data')
73
73
  if text is None: # or len(text) == 0:
74
74
  return ''
75
- alg = self.get_str(key='algorithm', default=None)
75
+ alg = self.get_str(key='algorithm')
76
76
  if alg is None or alg == EncodeAlgorithms.DEFAULT:
77
77
  alg = ''
78
78
  if len(alg) == 0:
@@ -85,7 +85,7 @@ class BaseDataWrapper(Dictionary):
85
85
  def encode(self, mime_type: str) -> str:
86
86
  """ toString(mimeType) """
87
87
  # get encoded data
88
- text = self.get_str(key='data', default=None)
88
+ text = self.get_str(key='data')
89
89
  if text is None: # or len(text) == 0:
90
90
  return ''
91
91
  alg = self.algorithm
@@ -98,7 +98,7 @@ class BaseDataWrapper(Dictionary):
98
98
 
99
99
  @property
100
100
  def algorithm(self) -> str:
101
- alg = self.get_str(key='algorithm', default=None)
101
+ alg = self.get_str(key='algorithm')
102
102
  if alg is None or len(alg) == 0:
103
103
  alg = EncodeAlgorithms.DEFAULT
104
104
  return alg
@@ -118,7 +118,7 @@ class BaseDataWrapper(Dictionary):
118
118
  def data(self) -> Optional[bytes]:
119
119
  binary = self.__data
120
120
  if binary is None:
121
- text = self.get_str(key='data', default=None)
121
+ text = self.get_str(key='data')
122
122
  if text is None or len(text) == 0:
123
123
  # assert False, 'TED data empty: %s' % self.dictionary
124
124
  return None
@@ -37,6 +37,7 @@ from ..protocol.forward import SecretContent, CombineForwardContent, ListContent
37
37
  from ..protocol.files import BaseFileContent, ImageFileContent, AudioFileContent, VideoFileContent
38
38
  from ..protocol.money import BaseMoneyContent, TransferMoneyContent
39
39
  from ..protocol.quote import BaseQuoteContent
40
+ from ..protocol.customized import AppCustomizedContent
40
41
 
41
42
  from ..protocol.base import BaseCommand
42
43
  from ..protocol.commands import BaseMetaCommand, BaseDocumentCommand
@@ -63,11 +64,12 @@ __all__ = [
63
64
  'InstantMessage', 'SecureMessage', 'ReliableMessage',
64
65
 
65
66
  # contents
66
- 'TextContent', 'ArrayContent', 'ForwardContent',
67
- 'PageContent', 'NameCard',
67
+ 'TextContent', 'PageContent', 'NameCard',
68
+ 'ForwardContent', 'CombineContent', 'ArrayContent',
68
69
  'FileContent', 'ImageContent', 'AudioContent', 'VideoContent',
69
70
  'MoneyContent', 'TransferContent',
70
- 'QuoteContent', 'CombineContent',
71
+ 'QuoteContent',
72
+ 'CustomizedContent',
71
73
 
72
74
  # commands
73
75
  'Command',
@@ -93,11 +95,12 @@ __all__ = [
93
95
 
94
96
  # contents
95
97
  'BaseContent',
96
- 'BaseTextContent', 'ListContent', 'SecretContent',
97
- 'WebPageContent', 'NameCardContent',
98
+ 'BaseTextContent', 'WebPageContent', 'NameCardContent',
99
+ 'SecretContent', 'CombineForwardContent', 'ListContent',
98
100
  'BaseFileContent', 'ImageFileContent', 'AudioFileContent', 'VideoFileContent',
99
101
  'BaseMoneyContent', 'TransferMoneyContent',
100
- 'BaseQuoteContent', 'CombineForwardContent',
102
+ 'BaseQuoteContent',
103
+ 'AppCustomizedContent',
101
104
 
102
105
  # commands
103
106
  'BaseCommand',
@@ -114,7 +114,7 @@ class BaseDocument(Dictionary, Document):
114
114
  :return: JsON string
115
115
  """
116
116
  if self.__json is None:
117
- self.__json = self.get_str(key='data', default=None)
117
+ self.__json = self.get_str(key='data')
118
118
  return self.__json
119
119
 
120
120
  @property # private
@@ -259,12 +259,12 @@ class BaseDocument(Dictionary, Document):
259
259
  @property # Override
260
260
  def time(self) -> Optional[DateTime]:
261
261
  seconds = self.get_property(name='time')
262
- return Converter.get_datetime(value=seconds, default=None)
262
+ return Converter.get_datetime(value=seconds)
263
263
 
264
264
  @property # Override
265
265
  def name(self) -> Optional[str]:
266
266
  text = self.get_property(name='name')
267
- return Converter.get_str(value=text, default=None)
267
+ return Converter.get_str(value=text)
268
268
 
269
269
  @name.setter # Override
270
270
  def name(self, text: str):
@@ -102,7 +102,7 @@ class MessageEnvelope(Dictionary, Envelope):
102
102
  @property # Override
103
103
  def time(self) -> Optional[DateTime]:
104
104
  if self.__time is None:
105
- self.__time = self.get_datetime(key='time', default=None)
105
+ self.__time = self.get_datetime(key='time')
106
106
  return self.__time
107
107
 
108
108
  @property # Override
@@ -116,7 +116,7 @@ class MessageEnvelope(Dictionary, Envelope):
116
116
 
117
117
  @property # Override
118
118
  def type(self) -> Optional[str]:
119
- return self.get_str(key='type', default=None)
119
+ return self.get_str(key='type')
120
120
 
121
121
  @type.setter # Override
122
122
  def type(self, value: str):
@@ -52,6 +52,7 @@ from .forward import ForwardContent, CombineContent, ArrayContent
52
52
  from .files import FileContent, ImageContent, AudioContent, VideoContent
53
53
  from .money import MoneyContent, TransferContent
54
54
  from .quote import QuoteContent
55
+ from .customized import CustomizedContent
55
56
 
56
57
  from .base import Command, CommandFactory
57
58
  from .commands import MetaCommand, DocumentCommand
@@ -108,6 +109,7 @@ __all__ = [
108
109
  'FileContent', 'ImageContent', 'AudioContent', 'VideoContent',
109
110
  'MoneyContent', 'TransferContent',
110
111
  'QuoteContent',
112
+ 'CustomizedContent',
111
113
 
112
114
  # commands
113
115
  'Command', # 'CommandFactory',
@@ -226,7 +226,7 @@ class BaseContent(Dictionary, Content):
226
226
  @property # Override
227
227
  def time(self) -> Optional[DateTime]:
228
228
  if self.__time is None:
229
- self.__time = self.get_datetime(key='time', default=None)
229
+ self.__time = self.get_datetime(key='time')
230
230
  return self.__time
231
231
 
232
232
  @property # Override
@@ -253,4 +253,4 @@ class BaseDocumentCommand(BaseMetaCommand, DocumentCommand):
253
253
 
254
254
  @property # Override
255
255
  def last_time(self) -> Optional[DateTime]:
256
- return self.get_datetime(key='last_time', default=None)
256
+ return self.get_datetime(key='last_time')
@@ -291,7 +291,7 @@ class WebPageContent(BaseContent, PageContent):
291
291
  def icon(self) -> Optional[PortableNetworkFile]:
292
292
  img = self.__icon
293
293
  if img is None:
294
- base64 = self.get_str(key='icon', default=None)
294
+ base64 = self.get_str(key='icon')
295
295
  img = self.__icon = PortableNetworkFile.parse(base64)
296
296
  return img
297
297
 
@@ -309,7 +309,7 @@ class WebPageContent(BaseContent, PageContent):
309
309
 
310
310
  @property # Override
311
311
  def desc(self) -> Optional[str]:
312
- return self.get_str(key='desc', default=None)
312
+ return self.get_str(key='desc')
313
313
 
314
314
  @desc.setter # Override
315
315
  def desc(self, text: Optional[str]):
@@ -325,7 +325,7 @@ class WebPageContent(BaseContent, PageContent):
325
325
  @property # Override
326
326
  def url(self) -> URI:
327
327
  # TODO: convert str to URI
328
- return self.get_str(key='URL', default=None)
328
+ return self.get_str(key='URL')
329
329
 
330
330
  @url.setter # Override
331
331
  def url(self, locator: URI):
@@ -338,7 +338,7 @@ class WebPageContent(BaseContent, PageContent):
338
338
 
339
339
  @property # Override
340
340
  def html(self) -> Optional[str]:
341
- return self.get_str(key='desc', default=None)
341
+ return self.get_str(key='desc')
342
342
 
343
343
  @html.setter # Override
344
344
  def html(self, content: Optional[str]):
@@ -0,0 +1,113 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # DIMP : Decentralized Instant Messaging Protocol
4
+ #
5
+ # Written in 2019 by Moky <albert.moky@gmail.com>
6
+ #
7
+ # ==============================================================================
8
+ # MIT License
9
+ #
10
+ # Copyright (c) 2019 Albert Moky
11
+ #
12
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ # of this software and associated documentation files (the "Software"), to deal
14
+ # in the Software without restriction, including without limitation the rights
15
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ # copies of the Software, and to permit persons to whom the Software is
17
+ # furnished to do so, subject to the following conditions:
18
+ #
19
+ # The above copyright notice and this permission notice shall be included in all
20
+ # copies or substantial portions of the Software.
21
+ #
22
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ # SOFTWARE.
29
+ # ==============================================================================
30
+
31
+ from abc import ABC, abstractmethod
32
+ from typing import Dict
33
+
34
+ from dkd import Content
35
+
36
+ from .types import ContentType
37
+ from .base import BaseContent
38
+
39
+
40
+ class CustomizedContent(Content, ABC):
41
+ """
42
+ Application Customized message
43
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
44
+
45
+ data format: {
46
+ type : i2s(0xCC),
47
+ sn : 123,
48
+
49
+ app : "{APP_ID}", // application (e.g.: "chat.dim.sechat")
50
+ mod : "{MODULE}", // module name (e.g.: "drift_bottle")
51
+ act : "{ACTION}", // action name (e.g.: "throw")
52
+ extra : info // action parameters
53
+ }
54
+ """
55
+
56
+ @property
57
+ @abstractmethod
58
+ def application(self) -> str:
59
+ """ App ID """
60
+ raise NotImplemented
61
+
62
+ @property
63
+ @abstractmethod
64
+ def module(self) -> str:
65
+ """ Module Name """
66
+ raise NotImplemented
67
+
68
+ @property
69
+ @abstractmethod
70
+ def action(self) -> str:
71
+ """ Action Name """
72
+ raise NotImplemented
73
+
74
+ #
75
+ # Factory method
76
+ #
77
+ @classmethod
78
+ def create(cls, app: str, mod: str, act: str):
79
+ return AppCustomizedContent(app=app, mod=mod, act=act)
80
+
81
+
82
+ class AppCustomizedContent(BaseContent, CustomizedContent):
83
+
84
+ def __init__(self, content: Dict = None,
85
+ msg_type: str = None,
86
+ app: str = None, mod: str = None, act: str = None):
87
+ if content is None:
88
+ # 1. new content with type, application, module & action
89
+ assert app is not None and mod is not None and act is not None, \
90
+ 'customized content error: %s, %s, %s, %s' % (msg_type, app, mod, act)
91
+ if msg_type is None:
92
+ msg_type = ContentType.CUSTOMIZED
93
+ super().__init__(None, msg_type)
94
+ self['app'] = app
95
+ self['mod'] = mod
96
+ self['act'] = act
97
+ else:
98
+ # 2. content info from network
99
+ assert msg_type is None and app is None and mod is None and act is None, \
100
+ 'params error: %s, %s, %s, %s, %s' % (content, msg_type, app, mod, act)
101
+ super().__init__(content)
102
+
103
+ @property # Override
104
+ def application(self) -> str:
105
+ return self.get_str(key='app', default='')
106
+
107
+ @property # Override
108
+ def module(self) -> str:
109
+ return self.get_str(key='mod', default='')
110
+
111
+ @property # Override
112
+ def action(self) -> str:
113
+ return self.get_str(key='act', default='')
@@ -341,7 +341,7 @@ class ImageFileContent(BaseFileContent, ImageContent):
341
341
  def thumbnail(self) -> Optional[PortableNetworkFile]:
342
342
  img = self.__thumbnail
343
343
  if img is None:
344
- base64 = self.get_str(key='thumbnail', default=None)
344
+ base64 = self.get_str(key='thumbnail')
345
345
  img = self.__thumbnail = PortableNetworkFile.parse(base64)
346
346
  return img
347
347
 
@@ -365,7 +365,7 @@ class AudioFileContent(BaseFileContent, AudioContent):
365
365
 
366
366
  @property # Override
367
367
  def text(self) -> Optional[str]:
368
- return self.get_str(key='text', default=None)
368
+ return self.get_str(key='text')
369
369
 
370
370
  @text.setter # Override
371
371
  def text(self, asr: str):
@@ -387,7 +387,7 @@ class VideoFileContent(BaseFileContent, VideoContent):
387
387
  def snapshot(self) -> Optional[PortableNetworkFile]:
388
388
  img = self.__snapshot
389
389
  if img is None:
390
- base64 = self.get_str(key='snapshot', default=None)
390
+ base64 = self.get_str(key='snapshot')
391
391
  img = self.__snapshot = PortableNetworkFile.parse(base64)
392
392
  return img
393
393
 
@@ -385,7 +385,7 @@ class QueryGroupCommand(BaseGroupCommand, QueryCommand):
385
385
 
386
386
  @property # Override
387
387
  def last_time(self) -> Optional[DateTime]:
388
- return self.get_datetime(key='last_time', default=None)
388
+ return self.get_datetime(key='last_time')
389
389
 
390
390
 
391
391
  class ResetGroupCommand(BaseGroupCommand, ResetCommand):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dimp
3
- Version: 2.3.2
3
+ Version: 2.3.3
4
4
  Summary: Decentralized Instant Messaging Protocol
5
5
  Home-page: https://github.com/dimchat/core-py
6
6
  Author: Albert Moky
@@ -45,8 +45,9 @@ License-File: LICENSE
45
45
  3. (S-C) handshake success
46
46
 
47
47
  ```python
48
+ from abc import ABC, abstractmethod
48
49
  from enum import IntEnum
49
- from typing import Optional, Any, Dict
50
+ from typing import Optional, Dict
50
51
 
51
52
  from dimp import Command, BaseCommand
52
53
 
@@ -58,7 +59,20 @@ class HandshakeState(IntEnum):
58
59
  Success = 3 # S -> C, handshake accepted
59
60
 
60
61
 
61
- class HandshakeCommand(BaseCommand):
62
+ def handshake_state(title: str, session: str = None) -> HandshakeState:
63
+ # Server -> Client
64
+ if title == 'DIM!': # or title == 'OK!':
65
+ return HandshakeState.Success
66
+ if title == 'DIM?':
67
+ return HandshakeState.Again
68
+ # Client -> Server: "Hello world!"
69
+ if session is None or len(session) == 0:
70
+ return HandshakeState.Start
71
+ else:
72
+ return HandshakeState.Restart
73
+
74
+
75
+ class HandshakeCommand(Command, ABC):
62
76
  """
63
77
  Handshake Command
64
78
  ~~~~~~~~~~~~~~~~~
@@ -74,37 +88,24 @@ class HandshakeCommand(BaseCommand):
74
88
  """
75
89
  HANDSHAKE = 'handshake'
76
90
 
77
- def __init__(self, content: Dict[str, Any] = None, title: str = None, session: str = None):
78
- if content is None:
79
- # 1. new command with title & session key
80
- assert title is not None, 'handshake command error: %s' % session
81
- cmd = self.HANDSHAKE
82
- super().__init__(cmd=cmd)
83
- self['title'] = title
84
- self['message'] = title # TODO: remove after all clients upgraded
85
- if session is not None:
86
- self['session'] = session
87
- else:
88
- # 2. command info from network
89
- assert title is None and session is None, 'params error: %s, %s, %s' % (content, title, session)
90
- super().__init__(content)
91
-
92
91
  @property
92
+ @abstractmethod
93
93
  def title(self) -> str:
94
- # TODO: modify after all clients upgraded
95
- text = self.get_str(key='title', default=None)
96
- if text is None:
97
- # compatible with v1.0
98
- text = self.get_str(key='message', default='')
99
- return text
94
+ raise NotImplemented
100
95
 
101
96
  @property
97
+ @abstractmethod
102
98
  def session(self) -> Optional[str]:
103
- return self.get_str(key='session', default=None)
99
+ raise NotImplemented
104
100
 
105
101
  @property
102
+ @abstractmethod
106
103
  def state(self) -> HandshakeState:
107
- return handshake_state(title=self.title, session=self.session)
104
+ raise NotImplemented
105
+
106
+ #
107
+ # Factories
108
+ #
108
109
 
109
110
  @classmethod
110
111
  def offer(cls, session: str = None) -> Command:
@@ -114,7 +115,7 @@ class HandshakeCommand(BaseCommand):
114
115
  :param session: Old session key
115
116
  :return: HandshakeCommand object
116
117
  """
117
- return HandshakeCommand(title='Hello world!', session=session)
118
+ return BaseHandshakeCommand(title='Hello world!', session=session)
118
119
 
119
120
  @classmethod
120
121
  def ask(cls, session: str) -> Command:
@@ -124,7 +125,7 @@ class HandshakeCommand(BaseCommand):
124
125
  :param session: New session key
125
126
  :return: HandshakeCommand object
126
127
  """
127
- return HandshakeCommand(title='DIM?', session=session)
128
+ return BaseHandshakeCommand(title='DIM?', session=session)
128
129
 
129
130
  @classmethod
130
131
  def accepted(cls, session: str = None) -> Command:
@@ -133,7 +134,7 @@ class HandshakeCommand(BaseCommand):
133
134
 
134
135
  :return: HandshakeCommand object
135
136
  """
136
- return HandshakeCommand(title='DIM!', session=session)
137
+ return BaseHandshakeCommand(title='DIM!', session=session)
137
138
 
138
139
  start = offer # (1. C->S) first handshake, without session
139
140
  again = ask # (2. S->C) ask client to handshake with new session key
@@ -141,17 +142,34 @@ class HandshakeCommand(BaseCommand):
141
142
  success = accepted # (4. S->C) notice the client that handshake accepted
142
143
 
143
144
 
144
- def handshake_state(title: str, session: str = None) -> HandshakeState:
145
- # Server -> Client
146
- if title == 'DIM!': # or title == 'OK!':
147
- return HandshakeState.Success
148
- if title == 'DIM?':
149
- return HandshakeState.Again
150
- # Client -> Server: "Hello world!"
151
- if session is None or len(session) == 0:
152
- return HandshakeState.Start
153
- else:
154
- return HandshakeState.Restart
145
+ class BaseHandshakeCommand(BaseCommand, HandshakeCommand):
146
+
147
+ def __init__(self, content: Dict = None, title: str = None, session: str = None):
148
+ if content is None:
149
+ # 1. new command with title & session key
150
+ assert title is not None, 'handshake command error: %s' % session
151
+ cmd = self.HANDSHAKE
152
+ super().__init__(cmd=cmd)
153
+ self['title'] = title
154
+ self['message'] = title # TODO: remove after all clients upgraded
155
+ if session is not None:
156
+ self['session'] = session
157
+ else:
158
+ # 2. command info from network
159
+ assert title is None and session is None, 'params error: %s, %s, %s' % (content, title, session)
160
+ super().__init__(content)
161
+
162
+ @property
163
+ def title(self) -> str:
164
+ return self.get_str(key='title', default='')
165
+
166
+ @property
167
+ def session(self) -> Optional[str]:
168
+ return self.get_str(key='session')
169
+
170
+ @property
171
+ def state(self) -> HandshakeState:
172
+ return handshake_state(title=self.title, session=self.session)
155
173
  ```
156
174
 
157
175
  ### extends Content
@@ -165,13 +183,13 @@ from dimp import Content
165
183
  from dimp import BaseContent
166
184
 
167
185
 
168
- class CustomizedContent(Content, ABC):
186
+ class AppContent(Content, ABC):
169
187
  """
170
188
  Application Customized message
171
189
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
172
190
 
173
191
  data format: {
174
- type : 0xCC,
192
+ type : 0xA0,
175
193
  sn : 123,
176
194
 
177
195
  app : "{APP_ID}", // application (e.g.: "chat.dim.sechat")
@@ -199,22 +217,25 @@ class CustomizedContent(Content, ABC):
199
217
  """ Action Name """
200
218
  raise NotImplemented
201
219
 
220
+ #
221
+ # Factory
222
+ #
202
223
  @classmethod
203
224
  def create(cls, app: str, mod: str, act: str):
204
- return AppCustomizedContent(app=app, mod=mod, act=act)
225
+ return ApplicationContent(app=app, mod=mod, act=act)
205
226
 
206
227
 
207
- class AppCustomizedContent(BaseContent, CustomizedContent):
228
+ class ApplicationContent(BaseContent, AppContent):
208
229
 
209
230
  def __init__(self, content: Dict[str, Any] = None,
210
- msg_type: int = None,
231
+ msg_type: str = None,
211
232
  app: str = None, mod: str = None, act: str = None):
212
233
  if content is None:
213
234
  # 1. new content with type, application, module & action
214
235
  assert app is not None and mod is not None and act is not None, \
215
236
  'customized content error: %s, %s, %s, %s' % (msg_type, app, mod, act)
216
237
  if msg_type is None:
217
- msg_type = ContentType.CUSTOMIZED.value
238
+ msg_type = ContentType.APPLICATION
218
239
  super().__init__(None, msg_type)
219
240
  self['app'] = app
220
241
  self['mod'] = mod
@@ -244,5 +265,5 @@ class AppCustomizedContent(BaseContent, CustomizedContent):
244
265
 
245
266
  ----
246
267
 
247
- Copyright &copy; 2018 Albert Moky
268
+ Copyright &copy; 2018-2025 Albert Moky
248
269
  [![Followers](https://img.shields.io/github/followers/moky)](https://github.com/moky?tab=followers)
@@ -28,6 +28,7 @@ dimp/protocol/__init__.py
28
28
  dimp/protocol/base.py
29
29
  dimp/protocol/commands.py
30
30
  dimp/protocol/contents.py
31
+ dimp/protocol/customized.py
31
32
  dimp/protocol/docs.py
32
33
  dimp/protocol/files.py
33
34
  dimp/protocol/forward.py
@@ -0,0 +1,2 @@
1
+ dkd>=2.3.3
2
+ mkm>=2.3.3
@@ -14,7 +14,7 @@ import io
14
14
 
15
15
  from setuptools import setup, find_packages
16
16
 
17
- __version__ = '2.3.2'
17
+ __version__ = '2.3.3'
18
18
  __author__ = 'Albert Moky'
19
19
  __contact__ = 'albert.moky@gmail.com'
20
20
 
@@ -38,7 +38,7 @@ setup(
38
38
  'Operating System :: OS Independent',
39
39
  ],
40
40
  install_requires=[
41
- 'dkd>=2.3.2',
42
- 'mkm>=2.3.2',
41
+ 'dkd>=2.3.3',
42
+ 'mkm>=2.3.3',
43
43
  ]
44
44
  )
@@ -1,2 +0,0 @@
1
- dkd>=2.3.2
2
- mkm>=2.3.2
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes