##// END OF EJS Templates
Rename "pull" request to "list"
neko259 -
r1566:a1b54223 default
parent child Browse files
Show More
@@ -1,85 +1,85 b''
1 import re
1 import re
2 import xml.etree.ElementTree as ET
2 import xml.etree.ElementTree as ET
3
3
4 import httplib2
4 import httplib2
5 from django.core.management import BaseCommand
5 from django.core.management import BaseCommand
6
6
7 from boards.models import GlobalId
7 from boards.models import GlobalId
8 from boards.models.post.sync import SyncManager
8 from boards.models.post.sync import SyncManager
9
9
10 __author__ = 'neko259'
10 __author__ = 'neko259'
11
11
12
12
13 REGEX_GLOBAL_ID = re.compile(r'(\w+)::([\w\+/]+)::(\d+)')
13 REGEX_GLOBAL_ID = re.compile(r'(\w+)::([\w\+/]+)::(\d+)')
14
14
15
15
16 class Command(BaseCommand):
16 class Command(BaseCommand):
17 help = 'Send a sync or get request to the server.'
17 help = 'Send a sync or get request to the server.'
18
18
19 def add_arguments(self, parser):
19 def add_arguments(self, parser):
20 parser.add_argument('url', type=str, help='Server root url')
20 parser.add_argument('url', type=str, help='Server root url')
21 parser.add_argument('--global-id', type=str, default='',
21 parser.add_argument('--global-id', type=str, default='',
22 help='Post global ID')
22 help='Post global ID')
23 parser.add_argument('--split-query', type=int,
23 parser.add_argument('--split-query', type=int,
24 help='Split GET query into separate by the given'
24 help='Split GET query into separate by the given'
25 ' number of posts in one')
25 ' number of posts in one')
26
26
27 def handle(self, *args, **options):
27 def handle(self, *args, **options):
28 url = options.get('url')
28 url = options.get('url')
29
29
30 pull_url = url + 'api/sync/pull/'
30 list_url = url + 'api/sync/list/'
31 get_url = url + 'api/sync/get/'
31 get_url = url + 'api/sync/get/'
32 file_url = url[:-1]
32 file_url = url[:-1]
33
33
34 global_id_str = options.get('global_id')
34 global_id_str = options.get('global_id')
35 if global_id_str:
35 if global_id_str:
36 match = REGEX_GLOBAL_ID.match(global_id_str)
36 match = REGEX_GLOBAL_ID.match(global_id_str)
37 if match:
37 if match:
38 key_type = match.group(1)
38 key_type = match.group(1)
39 key = match.group(2)
39 key = match.group(2)
40 local_id = match.group(3)
40 local_id = match.group(3)
41
41
42 global_id = GlobalId(key_type=key_type, key=key,
42 global_id = GlobalId(key_type=key_type, key=key,
43 local_id=local_id)
43 local_id=local_id)
44
44
45 xml = GlobalId.objects.generate_request_get([global_id])
45 xml = GlobalId.objects.generate_request_get([global_id])
46 # body = urllib.parse.urlencode(data)
46 # body = urllib.parse.urlencode(data)
47 h = httplib2.Http()
47 h = httplib2.Http()
48 response, content = h.request(get_url, method="POST", body=xml)
48 response, content = h.request(get_url, method="POST", body=xml)
49
49
50 SyncManager.parse_response_get(content, file_url)
50 SyncManager.parse_response_get(content, file_url)
51 else:
51 else:
52 raise Exception('Invalid global ID')
52 raise Exception('Invalid global ID')
53 else:
53 else:
54 h = httplib2.Http()
54 h = httplib2.Http()
55 xml = GlobalId.objects.generate_request_pull()
55 xml = GlobalId.objects.generate_request_list()
56 response, content = h.request(pull_url, method="POST", body=xml)
56 response, content = h.request(list_url, method="POST", body=xml)
57
57
58 print(content.decode() + '\n')
58 print(content.decode() + '\n')
59
59
60 root = ET.fromstring(content)
60 root = ET.fromstring(content)
61 status = root.findall('status')[0].text
61 status = root.findall('status')[0].text
62 if status == 'success':
62 if status == 'success':
63 ids_to_sync = list()
63 ids_to_sync = list()
64
64
65 models = root.findall('models')[0]
65 models = root.findall('models')[0]
66 for model in models:
66 for model in models:
67 global_id, exists = GlobalId.from_xml_element(model)
67 global_id, exists = GlobalId.from_xml_element(model)
68 if not exists:
68 if not exists:
69 print(global_id)
69 print(global_id)
70 ids_to_sync.append(global_id)
70 ids_to_sync.append(global_id)
71 print()
71 print()
72
72
73 if len(ids_to_sync) > 0:
73 if len(ids_to_sync) > 0:
74 limit = options.get('split_query', len(ids_to_sync))
74 limit = options.get('split_query', len(ids_to_sync))
75 for offset in range(0, len(ids_to_sync), limit):
75 for offset in range(0, len(ids_to_sync), limit):
76 xml = GlobalId.objects.generate_request_get(ids_to_sync[offset:offset+limit])
76 xml = GlobalId.objects.generate_request_get(ids_to_sync[offset:offset+limit])
77 # body = urllib.parse.urlencode(data)
77 # body = urllib.parse.urlencode(data)
78 h = httplib2.Http()
78 h = httplib2.Http()
79 response, content = h.request(get_url, method="POST", body=xml)
79 response, content = h.request(get_url, method="POST", body=xml)
80
80
81 SyncManager.parse_response_get(content, file_url)
81 SyncManager.parse_response_get(content, file_url)
82 else:
82 else:
83 print('Nothing to get, everything synced')
83 print('Nothing to get, everything synced')
84 else:
84 else:
85 raise Exception('Invalid response status')
85 raise Exception('Invalid response status')
@@ -1,284 +1,284 b''
1 import xml.etree.ElementTree as et
1 import xml.etree.ElementTree as et
2
2
3 from boards.models.attachment.downloaders import download
3 from boards.models.attachment.downloaders import download
4 from boards.utils import get_file_mimetype, get_file_hash
4 from boards.utils import get_file_mimetype, get_file_hash
5 from django.db import transaction
5 from django.db import transaction
6 from boards.models import KeyPair, GlobalId, Signature, Post, Tag
6 from boards.models import KeyPair, GlobalId, Signature, Post, Tag
7
7
8 EXCEPTION_NODE = 'Sync node returned an error: {}'
8 EXCEPTION_NODE = 'Sync node returned an error: {}'
9 EXCEPTION_OP = 'Load the OP first'
9 EXCEPTION_OP = 'Load the OP first'
10 EXCEPTION_DOWNLOAD = 'File was not downloaded'
10 EXCEPTION_DOWNLOAD = 'File was not downloaded'
11 EXCEPTION_HASH = 'File hash does not match attachment hash'
11 EXCEPTION_HASH = 'File hash does not match attachment hash'
12 EXCEPTION_SIGNATURE = 'Invalid model signature for {}'
12 EXCEPTION_SIGNATURE = 'Invalid model signature for {}'
13 ENCODING_UNICODE = 'unicode'
13 ENCODING_UNICODE = 'unicode'
14
14
15 TAG_MODEL = 'model'
15 TAG_MODEL = 'model'
16 TAG_REQUEST = 'request'
16 TAG_REQUEST = 'request'
17 TAG_RESPONSE = 'response'
17 TAG_RESPONSE = 'response'
18 TAG_ID = 'id'
18 TAG_ID = 'id'
19 TAG_STATUS = 'status'
19 TAG_STATUS = 'status'
20 TAG_MODELS = 'models'
20 TAG_MODELS = 'models'
21 TAG_TITLE = 'title'
21 TAG_TITLE = 'title'
22 TAG_TEXT = 'text'
22 TAG_TEXT = 'text'
23 TAG_THREAD = 'thread'
23 TAG_THREAD = 'thread'
24 TAG_PUB_TIME = 'pub-time'
24 TAG_PUB_TIME = 'pub-time'
25 TAG_SIGNATURES = 'signatures'
25 TAG_SIGNATURES = 'signatures'
26 TAG_SIGNATURE = 'signature'
26 TAG_SIGNATURE = 'signature'
27 TAG_CONTENT = 'content'
27 TAG_CONTENT = 'content'
28 TAG_ATTACHMENTS = 'attachments'
28 TAG_ATTACHMENTS = 'attachments'
29 TAG_ATTACHMENT = 'attachment'
29 TAG_ATTACHMENT = 'attachment'
30 TAG_TAGS = 'tags'
30 TAG_TAGS = 'tags'
31 TAG_TAG = 'tag'
31 TAG_TAG = 'tag'
32 TAG_ATTACHMENT_REFS = 'attachment-refs'
32 TAG_ATTACHMENT_REFS = 'attachment-refs'
33 TAG_ATTACHMENT_REF = 'attachment-ref'
33 TAG_ATTACHMENT_REF = 'attachment-ref'
34 TAG_TRIPCODE = 'tripcode'
34 TAG_TRIPCODE = 'tripcode'
35
35
36 TYPE_GET = 'get'
36 TYPE_GET = 'get'
37
37
38 ATTR_VERSION = 'version'
38 ATTR_VERSION = 'version'
39 ATTR_TYPE = 'type'
39 ATTR_TYPE = 'type'
40 ATTR_NAME = 'name'
40 ATTR_NAME = 'name'
41 ATTR_VALUE = 'value'
41 ATTR_VALUE = 'value'
42 ATTR_MIMETYPE = 'mimetype'
42 ATTR_MIMETYPE = 'mimetype'
43 ATTR_KEY = 'key'
43 ATTR_KEY = 'key'
44 ATTR_REF = 'ref'
44 ATTR_REF = 'ref'
45 ATTR_URL = 'url'
45 ATTR_URL = 'url'
46 ATTR_ID_TYPE = 'id-type'
46 ATTR_ID_TYPE = 'id-type'
47
47
48 ID_TYPE_MD5 = 'md5'
48 ID_TYPE_MD5 = 'md5'
49
49
50 STATUS_SUCCESS = 'success'
50 STATUS_SUCCESS = 'success'
51
51
52
52
53 class SyncException(Exception):
53 class SyncException(Exception):
54 pass
54 pass
55
55
56
56
57 class SyncManager:
57 class SyncManager:
58 @staticmethod
58 @staticmethod
59 def generate_response_get(model_list: list):
59 def generate_response_get(model_list: list):
60 response = et.Element(TAG_RESPONSE)
60 response = et.Element(TAG_RESPONSE)
61
61
62 status = et.SubElement(response, TAG_STATUS)
62 status = et.SubElement(response, TAG_STATUS)
63 status.text = STATUS_SUCCESS
63 status.text = STATUS_SUCCESS
64
64
65 models = et.SubElement(response, TAG_MODELS)
65 models = et.SubElement(response, TAG_MODELS)
66
66
67 for post in model_list:
67 for post in model_list:
68 model = et.SubElement(models, TAG_MODEL)
68 model = et.SubElement(models, TAG_MODEL)
69 model.set(ATTR_NAME, 'post')
69 model.set(ATTR_NAME, 'post')
70
70
71 global_id = post.global_id
71 global_id = post.global_id
72
72
73 images = post.images.all()
73 images = post.images.all()
74 attachments = post.attachments.all()
74 attachments = post.attachments.all()
75 if global_id.content:
75 if global_id.content:
76 model.append(et.fromstring(global_id.content))
76 model.append(et.fromstring(global_id.content))
77 if len(images) > 0 or len(attachments) > 0:
77 if len(images) > 0 or len(attachments) > 0:
78 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
78 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
79 for image in images:
79 for image in images:
80 SyncManager._attachment_to_xml(
80 SyncManager._attachment_to_xml(
81 None, attachment_refs, image.image.file,
81 None, attachment_refs, image.image.file,
82 image.hash, image.image.url)
82 image.hash, image.image.url)
83 for file in attachments:
83 for file in attachments:
84 SyncManager._attachment_to_xml(
84 SyncManager._attachment_to_xml(
85 None, attachment_refs, file.file.file,
85 None, attachment_refs, file.file.file,
86 file.hash, file.file.url)
86 file.hash, file.file.url)
87 else:
87 else:
88 content_tag = et.SubElement(model, TAG_CONTENT)
88 content_tag = et.SubElement(model, TAG_CONTENT)
89
89
90 tag_id = et.SubElement(content_tag, TAG_ID)
90 tag_id = et.SubElement(content_tag, TAG_ID)
91 global_id.to_xml_element(tag_id)
91 global_id.to_xml_element(tag_id)
92
92
93 title = et.SubElement(content_tag, TAG_TITLE)
93 title = et.SubElement(content_tag, TAG_TITLE)
94 title.text = post.title
94 title.text = post.title
95
95
96 text = et.SubElement(content_tag, TAG_TEXT)
96 text = et.SubElement(content_tag, TAG_TEXT)
97 text.text = post.get_sync_text()
97 text.text = post.get_sync_text()
98
98
99 thread = post.get_thread()
99 thread = post.get_thread()
100 if post.is_opening():
100 if post.is_opening():
101 tag_tags = et.SubElement(content_tag, TAG_TAGS)
101 tag_tags = et.SubElement(content_tag, TAG_TAGS)
102 for tag in thread.get_tags():
102 for tag in thread.get_tags():
103 tag_tag = et.SubElement(tag_tags, TAG_TAG)
103 tag_tag = et.SubElement(tag_tags, TAG_TAG)
104 tag_tag.text = tag.name
104 tag_tag.text = tag.name
105 else:
105 else:
106 tag_thread = et.SubElement(content_tag, TAG_THREAD)
106 tag_thread = et.SubElement(content_tag, TAG_THREAD)
107 thread_id = et.SubElement(tag_thread, TAG_ID)
107 thread_id = et.SubElement(tag_thread, TAG_ID)
108 thread.get_opening_post().global_id.to_xml_element(thread_id)
108 thread.get_opening_post().global_id.to_xml_element(thread_id)
109
109
110 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
110 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
111 pub_time.text = str(post.get_pub_time_str())
111 pub_time.text = str(post.get_pub_time_str())
112
112
113 if post.tripcode:
113 if post.tripcode:
114 tripcode = et.SubElement(content_tag, TAG_TRIPCODE)
114 tripcode = et.SubElement(content_tag, TAG_TRIPCODE)
115 tripcode.text = post.tripcode
115 tripcode.text = post.tripcode
116
116
117 if len(images) > 0 or len(attachments) > 0:
117 if len(images) > 0 or len(attachments) > 0:
118 attachments_tag = et.SubElement(content_tag, TAG_ATTACHMENTS)
118 attachments_tag = et.SubElement(content_tag, TAG_ATTACHMENTS)
119 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
119 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
120
120
121 for image in images:
121 for image in images:
122 SyncManager._attachment_to_xml(
122 SyncManager._attachment_to_xml(
123 attachments_tag, attachment_refs, image.image.file,
123 attachments_tag, attachment_refs, image.image.file,
124 image.hash, image.image.url)
124 image.hash, image.image.url)
125 for file in attachments:
125 for file in attachments:
126 SyncManager._attachment_to_xml(
126 SyncManager._attachment_to_xml(
127 attachments_tag, attachment_refs, file.file.file,
127 attachments_tag, attachment_refs, file.file.file,
128 file.hash, file.file.url)
128 file.hash, file.file.url)
129
129
130 global_id.content = et.tostring(content_tag, ENCODING_UNICODE)
130 global_id.content = et.tostring(content_tag, ENCODING_UNICODE)
131 global_id.save()
131 global_id.save()
132
132
133 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
133 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
134 post_signatures = global_id.signature_set.all()
134 post_signatures = global_id.signature_set.all()
135 if post_signatures:
135 if post_signatures:
136 signatures = post_signatures
136 signatures = post_signatures
137 else:
137 else:
138 key = KeyPair.objects.get(public_key=global_id.key)
138 key = KeyPair.objects.get(public_key=global_id.key)
139 signature = Signature(
139 signature = Signature(
140 key_type=key.key_type,
140 key_type=key.key_type,
141 key=key.public_key,
141 key=key.public_key,
142 signature=key.sign(global_id.content),
142 signature=key.sign(global_id.content),
143 global_id=global_id,
143 global_id=global_id,
144 )
144 )
145 signature.save()
145 signature.save()
146 signatures = [signature]
146 signatures = [signature]
147 for signature in signatures:
147 for signature in signatures:
148 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
148 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
149 signature_tag.set(ATTR_TYPE, signature.key_type)
149 signature_tag.set(ATTR_TYPE, signature.key_type)
150 signature_tag.set(ATTR_VALUE, signature.signature)
150 signature_tag.set(ATTR_VALUE, signature.signature)
151 signature_tag.set(ATTR_KEY, signature.key)
151 signature_tag.set(ATTR_KEY, signature.key)
152
152
153 return et.tostring(response, ENCODING_UNICODE)
153 return et.tostring(response, ENCODING_UNICODE)
154
154
155 @staticmethod
155 @staticmethod
156 @transaction.atomic
156 @transaction.atomic
157 def parse_response_get(response_xml, hostname):
157 def parse_response_get(response_xml, hostname):
158 tag_root = et.fromstring(response_xml)
158 tag_root = et.fromstring(response_xml)
159 tag_status = tag_root.find(TAG_STATUS)
159 tag_status = tag_root.find(TAG_STATUS)
160 if STATUS_SUCCESS == tag_status.text:
160 if STATUS_SUCCESS == tag_status.text:
161 tag_models = tag_root.find(TAG_MODELS)
161 tag_models = tag_root.find(TAG_MODELS)
162 for tag_model in tag_models:
162 for tag_model in tag_models:
163 tag_content = tag_model.find(TAG_CONTENT)
163 tag_content = tag_model.find(TAG_CONTENT)
164
164
165 content_str = et.tostring(tag_content, ENCODING_UNICODE)
165 content_str = et.tostring(tag_content, ENCODING_UNICODE)
166 signatures = SyncManager._verify_model(content_str, tag_model)
166 signatures = SyncManager._verify_model(content_str, tag_model)
167
167
168 tag_id = tag_content.find(TAG_ID)
168 tag_id = tag_content.find(TAG_ID)
169 global_id, exists = GlobalId.from_xml_element(tag_id)
169 global_id, exists = GlobalId.from_xml_element(tag_id)
170
170
171 if exists:
171 if exists:
172 print('Post with same ID already exists')
172 print('Post with same ID already exists')
173 else:
173 else:
174 global_id.content = content_str
174 global_id.content = content_str
175 global_id.save()
175 global_id.save()
176 for signature in signatures:
176 for signature in signatures:
177 signature.global_id = global_id
177 signature.global_id = global_id
178 signature.save()
178 signature.save()
179
179
180 title = tag_content.find(TAG_TITLE).text or ''
180 title = tag_content.find(TAG_TITLE).text or ''
181 text = tag_content.find(TAG_TEXT).text or ''
181 text = tag_content.find(TAG_TEXT).text or ''
182 pub_time = tag_content.find(TAG_PUB_TIME).text
182 pub_time = tag_content.find(TAG_PUB_TIME).text
183 tripcode_tag = tag_content.find(TAG_TRIPCODE)
183 tripcode_tag = tag_content.find(TAG_TRIPCODE)
184 if tripcode_tag is not None:
184 if tripcode_tag is not None:
185 tripcode = tripcode_tag.text or ''
185 tripcode = tripcode_tag.text or ''
186 else:
186 else:
187 tripcode = ''
187 tripcode = ''
188
188
189 thread = tag_content.find(TAG_THREAD)
189 thread = tag_content.find(TAG_THREAD)
190 tags = []
190 tags = []
191 if thread:
191 if thread:
192 thread_id = thread.find(TAG_ID)
192 thread_id = thread.find(TAG_ID)
193 op_global_id, exists = GlobalId.from_xml_element(thread_id)
193 op_global_id, exists = GlobalId.from_xml_element(thread_id)
194 if exists:
194 if exists:
195 opening_post = Post.objects.get(global_id=op_global_id)
195 opening_post = Post.objects.get(global_id=op_global_id)
196 else:
196 else:
197 raise SyncException(EXCEPTION_OP)
197 raise SyncException(EXCEPTION_OP)
198 else:
198 else:
199 opening_post = None
199 opening_post = None
200 tag_tags = tag_content.find(TAG_TAGS)
200 tag_tags = tag_content.find(TAG_TAGS)
201 for tag_tag in tag_tags:
201 for tag_tag in tag_tags:
202 tag, created = Tag.objects.get_or_create(
202 tag, created = Tag.objects.get_or_create(
203 name=tag_tag.text)
203 name=tag_tag.text)
204 tags.append(tag)
204 tags.append(tag)
205
205
206 # TODO Check that the replied posts are already present
206 # TODO Check that the replied posts are already present
207 # before adding new ones
207 # before adding new ones
208
208
209 files = []
209 files = []
210 tag_attachments = tag_content.find(TAG_ATTACHMENTS) or list()
210 tag_attachments = tag_content.find(TAG_ATTACHMENTS) or list()
211 tag_refs = tag_model.find(TAG_ATTACHMENT_REFS)
211 tag_refs = tag_model.find(TAG_ATTACHMENT_REFS)
212 for attachment in tag_attachments:
212 for attachment in tag_attachments:
213 tag_ref = tag_refs.find("{}[@ref='{}']".format(
213 tag_ref = tag_refs.find("{}[@ref='{}']".format(
214 TAG_ATTACHMENT_REF, attachment.text))
214 TAG_ATTACHMENT_REF, attachment.text))
215 url = tag_ref.get(ATTR_URL)
215 url = tag_ref.get(ATTR_URL)
216 attached_file = download(hostname + url)
216 attached_file = download(hostname + url)
217 if attached_file is None:
217 if attached_file is None:
218 raise SyncException(EXCEPTION_DOWNLOAD)
218 raise SyncException(EXCEPTION_DOWNLOAD)
219
219
220 hash = get_file_hash(attached_file)
220 hash = get_file_hash(attached_file)
221 if hash != attachment.text:
221 if hash != attachment.text:
222 raise SyncException(EXCEPTION_HASH)
222 raise SyncException(EXCEPTION_HASH)
223
223
224 files.append(attached_file)
224 files.append(attached_file)
225
225
226 Post.objects.import_post(
226 Post.objects.import_post(
227 title=title, text=text, pub_time=pub_time,
227 title=title, text=text, pub_time=pub_time,
228 opening_post=opening_post, tags=tags,
228 opening_post=opening_post, tags=tags,
229 global_id=global_id, files=files, tripcode=tripcode)
229 global_id=global_id, files=files, tripcode=tripcode)
230 else:
230 else:
231 raise SyncException(EXCEPTION_NODE.format(tag_status.text))
231 raise SyncException(EXCEPTION_NODE.format(tag_status.text))
232
232
233 @staticmethod
233 @staticmethod
234 def generate_response_pull():
234 def generate_response_list():
235 response = et.Element(TAG_RESPONSE)
235 response = et.Element(TAG_RESPONSE)
236
236
237 status = et.SubElement(response, TAG_STATUS)
237 status = et.SubElement(response, TAG_STATUS)
238 status.text = STATUS_SUCCESS
238 status.text = STATUS_SUCCESS
239
239
240 models = et.SubElement(response, TAG_MODELS)
240 models = et.SubElement(response, TAG_MODELS)
241
241
242 for post in Post.objects.prefetch_related('global_id').all():
242 for post in Post.objects.prefetch_related('global_id').all():
243 tag_id = et.SubElement(models, TAG_ID)
243 tag_id = et.SubElement(models, TAG_ID)
244 post.global_id.to_xml_element(tag_id)
244 post.global_id.to_xml_element(tag_id)
245
245
246 return et.tostring(response, ENCODING_UNICODE)
246 return et.tostring(response, ENCODING_UNICODE)
247
247
248 @staticmethod
248 @staticmethod
249 def _verify_model(content_str, tag_model):
249 def _verify_model(content_str, tag_model):
250 """
250 """
251 Verifies all signatures for a single model.
251 Verifies all signatures for a single model.
252 """
252 """
253
253
254 signatures = []
254 signatures = []
255
255
256 tag_signatures = tag_model.find(TAG_SIGNATURES)
256 tag_signatures = tag_model.find(TAG_SIGNATURES)
257 for tag_signature in tag_signatures:
257 for tag_signature in tag_signatures:
258 signature_type = tag_signature.get(ATTR_TYPE)
258 signature_type = tag_signature.get(ATTR_TYPE)
259 signature_value = tag_signature.get(ATTR_VALUE)
259 signature_value = tag_signature.get(ATTR_VALUE)
260 signature_key = tag_signature.get(ATTR_KEY)
260 signature_key = tag_signature.get(ATTR_KEY)
261
261
262 signature = Signature(key_type=signature_type,
262 signature = Signature(key_type=signature_type,
263 key=signature_key,
263 key=signature_key,
264 signature=signature_value)
264 signature=signature_value)
265
265
266 if not KeyPair.objects.verify(signature, content_str):
266 if not KeyPair.objects.verify(signature, content_str):
267 raise SyncException(EXCEPTION_SIGNATURE.format(content_str))
267 raise SyncException(EXCEPTION_SIGNATURE.format(content_str))
268
268
269 signatures.append(signature)
269 signatures.append(signature)
270
270
271 return signatures
271 return signatures
272
272
273 @staticmethod
273 @staticmethod
274 def _attachment_to_xml(tag_attachments, tag_refs, file, hash, url):
274 def _attachment_to_xml(tag_attachments, tag_refs, file, hash, url):
275 if tag_attachments is not None:
275 if tag_attachments is not None:
276 mimetype = get_file_mimetype(file)
276 mimetype = get_file_mimetype(file)
277 attachment = et.SubElement(tag_attachments, TAG_ATTACHMENT)
277 attachment = et.SubElement(tag_attachments, TAG_ATTACHMENT)
278 attachment.set(ATTR_MIMETYPE, mimetype)
278 attachment.set(ATTR_MIMETYPE, mimetype)
279 attachment.set(ATTR_ID_TYPE, ID_TYPE_MD5)
279 attachment.set(ATTR_ID_TYPE, ID_TYPE_MD5)
280 attachment.text = hash
280 attachment.text = hash
281
281
282 attachment_ref = et.SubElement(tag_refs, TAG_ATTACHMENT_REF)
282 attachment_ref = et.SubElement(tag_refs, TAG_ATTACHMENT_REF)
283 attachment_ref.set(ATTR_REF, hash)
283 attachment_ref.set(ATTR_REF, hash)
284 attachment_ref.set(ATTR_URL, url)
284 attachment_ref.set(ATTR_URL, url)
@@ -1,150 +1,150 b''
1 import xml.etree.ElementTree as et
1 import xml.etree.ElementTree as et
2 from django.db import models
2 from django.db import models
3 from boards.models import KeyPair
3 from boards.models import KeyPair
4
4
5
5
6 TAG_MODEL = 'model'
6 TAG_MODEL = 'model'
7 TAG_REQUEST = 'request'
7 TAG_REQUEST = 'request'
8 TAG_ID = 'id'
8 TAG_ID = 'id'
9
9
10 TYPE_GET = 'get'
10 TYPE_GET = 'get'
11 TYPE_PULL = 'pull'
11 TYPE_LIST = 'list'
12
12
13 ATTR_VERSION = 'version'
13 ATTR_VERSION = 'version'
14 ATTR_TYPE = 'type'
14 ATTR_TYPE = 'type'
15 ATTR_NAME = 'name'
15 ATTR_NAME = 'name'
16
16
17 ATTR_KEY = 'key'
17 ATTR_KEY = 'key'
18 ATTR_KEY_TYPE = 'type'
18 ATTR_KEY_TYPE = 'type'
19 ATTR_LOCAL_ID = 'local-id'
19 ATTR_LOCAL_ID = 'local-id'
20
20
21
21
22 class GlobalIdManager(models.Manager):
22 class GlobalIdManager(models.Manager):
23 def generate_request_get(self, global_id_list: list):
23 def generate_request_get(self, global_id_list: list):
24 """
24 """
25 Form a get request from a list of ModelId objects.
25 Form a get request from a list of ModelId objects.
26 """
26 """
27
27
28 request = et.Element(TAG_REQUEST)
28 request = et.Element(TAG_REQUEST)
29 request.set(ATTR_TYPE, TYPE_GET)
29 request.set(ATTR_TYPE, TYPE_GET)
30 request.set(ATTR_VERSION, '1.0')
30 request.set(ATTR_VERSION, '1.0')
31
31
32 model = et.SubElement(request, TAG_MODEL)
32 model = et.SubElement(request, TAG_MODEL)
33 model.set(ATTR_VERSION, '1.0')
33 model.set(ATTR_VERSION, '1.0')
34 model.set(ATTR_NAME, 'post')
34 model.set(ATTR_NAME, 'post')
35
35
36 for global_id in global_id_list:
36 for global_id in global_id_list:
37 tag_id = et.SubElement(model, TAG_ID)
37 tag_id = et.SubElement(model, TAG_ID)
38 global_id.to_xml_element(tag_id)
38 global_id.to_xml_element(tag_id)
39
39
40 return et.tostring(request, 'unicode')
40 return et.tostring(request, 'unicode')
41
41
42 def generate_request_pull(self):
42 def generate_request_list(self):
43 """
43 """
44 Form a pull request from a list of ModelId objects.
44 Form a pull request from a list of ModelId objects.
45 """
45 """
46
46
47 request = et.Element(TAG_REQUEST)
47 request = et.Element(TAG_REQUEST)
48 request.set(ATTR_TYPE, TYPE_PULL)
48 request.set(ATTR_TYPE, TYPE_LIST)
49 request.set(ATTR_VERSION, '1.0')
49 request.set(ATTR_VERSION, '1.0')
50
50
51 model = et.SubElement(request, TAG_MODEL)
51 model = et.SubElement(request, TAG_MODEL)
52 model.set(ATTR_VERSION, '1.0')
52 model.set(ATTR_VERSION, '1.0')
53 model.set(ATTR_NAME, 'post')
53 model.set(ATTR_NAME, 'post')
54
54
55 return et.tostring(request, 'unicode')
55 return et.tostring(request, 'unicode')
56
56
57 def global_id_exists(self, global_id):
57 def global_id_exists(self, global_id):
58 """
58 """
59 Checks if the same global id already exists in the system.
59 Checks if the same global id already exists in the system.
60 """
60 """
61
61
62 return self.filter(key=global_id.key,
62 return self.filter(key=global_id.key,
63 key_type=global_id.key_type,
63 key_type=global_id.key_type,
64 local_id=global_id.local_id).exists()
64 local_id=global_id.local_id).exists()
65
65
66
66
67 class GlobalId(models.Model):
67 class GlobalId(models.Model):
68 """
68 """
69 Global model ID and cache.
69 Global model ID and cache.
70 Key, key type and local ID make a single global identificator of the model.
70 Key, key type and local ID make a single global identificator of the model.
71 Content is an XML cache of the model that can be passed along between nodes
71 Content is an XML cache of the model that can be passed along between nodes
72 without manual serialization each time.
72 without manual serialization each time.
73 """
73 """
74 class Meta:
74 class Meta:
75 app_label = 'boards'
75 app_label = 'boards'
76
76
77 objects = GlobalIdManager()
77 objects = GlobalIdManager()
78
78
79 def __init__(self, *args, **kwargs):
79 def __init__(self, *args, **kwargs):
80 models.Model.__init__(self, *args, **kwargs)
80 models.Model.__init__(self, *args, **kwargs)
81
81
82 if 'key' in kwargs and 'key_type' in kwargs and 'local_id' in kwargs:
82 if 'key' in kwargs and 'key_type' in kwargs and 'local_id' in kwargs:
83 self.key = kwargs['key']
83 self.key = kwargs['key']
84 self.key_type = kwargs['key_type']
84 self.key_type = kwargs['key_type']
85 self.local_id = kwargs['local_id']
85 self.local_id = kwargs['local_id']
86
86
87 key = models.TextField()
87 key = models.TextField()
88 key_type = models.TextField()
88 key_type = models.TextField()
89 local_id = models.IntegerField()
89 local_id = models.IntegerField()
90 content = models.TextField(blank=True, null=True)
90 content = models.TextField(blank=True, null=True)
91
91
92 def __str__(self):
92 def __str__(self):
93 return '%s::%s::%d' % (self.key_type, self.key, self.local_id)
93 return '%s::%s::%d' % (self.key_type, self.key, self.local_id)
94
94
95 def to_xml_element(self, element: et.Element):
95 def to_xml_element(self, element: et.Element):
96 """
96 """
97 Exports global id to an XML element.
97 Exports global id to an XML element.
98 """
98 """
99
99
100 element.set(ATTR_KEY, self.key)
100 element.set(ATTR_KEY, self.key)
101 element.set(ATTR_KEY_TYPE, self.key_type)
101 element.set(ATTR_KEY_TYPE, self.key_type)
102 element.set(ATTR_LOCAL_ID, str(self.local_id))
102 element.set(ATTR_LOCAL_ID, str(self.local_id))
103
103
104 @staticmethod
104 @staticmethod
105 def from_xml_element(element: et.Element):
105 def from_xml_element(element: et.Element):
106 """
106 """
107 Parses XML id tag and gets global id from it.
107 Parses XML id tag and gets global id from it.
108
108
109 Arguments:
109 Arguments:
110 element -- the XML 'id' element
110 element -- the XML 'id' element
111
111
112 Returns:
112 Returns:
113 global_id -- id itself
113 global_id -- id itself
114 exists -- True if the global id was taken from database, False if it
114 exists -- True if the global id was taken from database, False if it
115 did not exist and was created.
115 did not exist and was created.
116 """
116 """
117
117
118 try:
118 try:
119 return GlobalId.objects.get(key=element.get(ATTR_KEY),
119 return GlobalId.objects.get(key=element.get(ATTR_KEY),
120 key_type=element.get(ATTR_KEY_TYPE),
120 key_type=element.get(ATTR_KEY_TYPE),
121 local_id=int(element.get(
121 local_id=int(element.get(
122 ATTR_LOCAL_ID))), True
122 ATTR_LOCAL_ID))), True
123 except GlobalId.DoesNotExist:
123 except GlobalId.DoesNotExist:
124 return GlobalId(key=element.get(ATTR_KEY),
124 return GlobalId(key=element.get(ATTR_KEY),
125 key_type=element.get(ATTR_KEY_TYPE),
125 key_type=element.get(ATTR_KEY_TYPE),
126 local_id=int(element.get(ATTR_LOCAL_ID))), False
126 local_id=int(element.get(ATTR_LOCAL_ID))), False
127
127
128 def is_local(self):
128 def is_local(self):
129 """Checks fo the ID is local model's"""
129 """Checks fo the ID is local model's"""
130 return KeyPair.objects.filter(
130 return KeyPair.objects.filter(
131 key_type=self.key_type, public_key=self.key).exists()
131 key_type=self.key_type, public_key=self.key).exists()
132
132
133
133
134 class Signature(models.Model):
134 class Signature(models.Model):
135 class Meta:
135 class Meta:
136 app_label = 'boards'
136 app_label = 'boards'
137
137
138 def __init__(self, *args, **kwargs):
138 def __init__(self, *args, **kwargs):
139 models.Model.__init__(self, *args, **kwargs)
139 models.Model.__init__(self, *args, **kwargs)
140
140
141 if 'key' in kwargs and 'key_type' in kwargs and 'signature' in kwargs:
141 if 'key' in kwargs and 'key_type' in kwargs and 'signature' in kwargs:
142 self.key_type = kwargs['key_type']
142 self.key_type = kwargs['key_type']
143 self.key = kwargs['key']
143 self.key = kwargs['key']
144 self.signature = kwargs['signature']
144 self.signature = kwargs['signature']
145
145
146 key_type = models.TextField()
146 key_type = models.TextField()
147 key = models.TextField()
147 key = models.TextField()
148 signature = models.TextField()
148 signature = models.TextField()
149
149
150 global_id = models.ForeignKey('GlobalId')
150 global_id = models.ForeignKey('GlobalId')
@@ -1,99 +1,96 b''
1 from django.conf.urls import url
1 from django.conf.urls import url
2 #from django.views.i18n import javascript_catalog
3
2
4 import neboard
3 import neboard
5
4
6 from boards import views
5 from boards import views
7 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
6 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
8 from boards.views import api, tag_threads, all_threads, \
7 from boards.views import api, tag_threads, all_threads, \
9 settings, all_tags, feed
8 settings, all_tags, feed
10 from boards.views.authors import AuthorsView
9 from boards.views.authors import AuthorsView
11 from boards.views.notifications import NotificationView
10 from boards.views.notifications import NotificationView
12 from boards.views.search import BoardSearchView
13 from boards.views.static import StaticPageView
11 from boards.views.static import StaticPageView
14 from boards.views.preview import PostPreviewView
12 from boards.views.preview import PostPreviewView
15 from boards.views.sync import get_post_sync_data, response_get, response_pull
13 from boards.views.sync import get_post_sync_data, response_get, response_list
16 from boards.views.random import RandomImageView
14 from boards.views.random import RandomImageView
17 from boards.views.tag_gallery import TagGalleryView
15 from boards.views.tag_gallery import TagGalleryView
18 from boards.views.translation import cached_javascript_catalog
16 from boards.views.translation import cached_javascript_catalog
19
17
20
18
21 js_info_dict = {
19 js_info_dict = {
22 'packages': ('boards',),
20 'packages': ('boards',),
23 }
21 }
24
22
25 urlpatterns = [
23 urlpatterns = [
26 # /boards/
24 # /boards/
27 url(r'^$', all_threads.AllThreadsView.as_view(), name='index'),
25 url(r'^$', all_threads.AllThreadsView.as_view(), name='index'),
28
26
29 # /boards/tag/tag_name/
27 # /boards/tag/tag_name/
30 url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(),
28 url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(),
31 name='tag'),
29 name='tag'),
32
30
33 # /boards/thread/
31 # /boards/thread/
34 url(r'^thread/(?P<post_id>\d+)/$', views.thread.NormalThreadView.as_view(),
32 url(r'^thread/(?P<post_id>\d+)/$', views.thread.NormalThreadView.as_view(),
35 name='thread'),
33 name='thread'),
36 url(r'^thread/(?P<post_id>\d+)/mode/gallery/$', views.thread.GalleryThreadView.as_view(),
34 url(r'^thread/(?P<post_id>\d+)/mode/gallery/$', views.thread.GalleryThreadView.as_view(),
37 name='thread_gallery'),
35 name='thread_gallery'),
38 url(r'^thread/(?P<post_id>\d+)/mode/tree/$', views.thread.TreeThreadView.as_view(),
36 url(r'^thread/(?P<post_id>\d+)/mode/tree/$', views.thread.TreeThreadView.as_view(),
39 name='thread_tree'),
37 name='thread_tree'),
40 # /feed/
38 # /feed/
41 url(r'^feed/$', views.feed.FeedView.as_view(), name='feed'),
39 url(r'^feed/$', views.feed.FeedView.as_view(), name='feed'),
42
40
43 url(r'^settings/$', settings.SettingsView.as_view(), name='settings'),
41 url(r'^settings/$', settings.SettingsView.as_view(), name='settings'),
44 url(r'^tags/(?P<query>\w+)?/?$', all_tags.AllTagsView.as_view(), name='tags'),
42 url(r'^tags/(?P<query>\w+)?/?$', all_tags.AllTagsView.as_view(), name='tags'),
45 url(r'^authors/$', AuthorsView.as_view(), name='authors'),
43 url(r'^authors/$', AuthorsView.as_view(), name='authors'),
46
44
47 url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'),
45 url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'),
48 url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(),
46 url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(),
49 name='staticpage'),
47 name='staticpage'),
50
48
51 url(r'^random/$', RandomImageView.as_view(), name='random'),
49 url(r'^random/$', RandomImageView.as_view(), name='random'),
52 url(r'^tag/(?P<tag_name>\w+)/gallery/$', TagGalleryView.as_view(), name='tag_gallery'),
50 url(r'^tag/(?P<tag_name>\w+)/gallery/$', TagGalleryView.as_view(), name='tag_gallery'),
53
51
54 # RSS feeds
52 # RSS feeds
55 url(r'^rss/$', AllThreadsFeed()),
53 url(r'^rss/$', AllThreadsFeed()),
56 url(r'^page/(?P<page>\d+)/rss/$', AllThreadsFeed()),
54 url(r'^page/(?P<page>\d+)/rss/$', AllThreadsFeed()),
57 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
55 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
58 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
56 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
59 url(r'^thread/(?P<post_id>\d+)/rss/$', ThreadPostsFeed()),
57 url(r'^thread/(?P<post_id>\d+)/rss/$', ThreadPostsFeed()),
60
58
61 # i18n
59 # i18n
62 url(r'^jsi18n/$', cached_javascript_catalog, js_info_dict,
60 url(r'^jsi18n/$', cached_javascript_catalog, js_info_dict,
63 name='js_info_dict'),
61 name='js_info_dict'),
64
62
65 # API
63 # API
66 url(r'^api/post/(?P<post_id>\d+)/$', api.get_post, name="get_post"),
64 url(r'^api/post/(?P<post_id>\d+)/$', api.get_post, name="get_post"),
67 url(r'^api/diff_thread/$', api.api_get_threaddiff, name="get_thread_diff"),
65 url(r'^api/diff_thread/$', api.api_get_threaddiff, name="get_thread_diff"),
68 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
66 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
69 name='get_threads'),
67 name='get_threads'),
70 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
68 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
71 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
69 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
72 name='get_thread'),
70 name='get_thread'),
73 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
71 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
74 name='add_post'),
72 name='add_post'),
75 url(r'^api/notifications/(?P<username>\w+)/$', api.api_get_notifications,
73 url(r'^api/notifications/(?P<username>\w+)/$', api.api_get_notifications,
76 name='api_notifications'),
74 name='api_notifications'),
77 url(r'^api/preview/$', api.api_get_preview, name='preview'),
75 url(r'^api/preview/$', api.api_get_preview, name='preview'),
78 url(r'^api/new_posts/$', api.api_get_new_posts, name='new_posts'),
76 url(r'^api/new_posts/$', api.api_get_new_posts, name='new_posts'),
79
77
80 # Sync protocol API
78 # Sync protocol API
81 url(r'^api/sync/pull/$', response_pull, name='api_sync_pull'),
79 url(r'^api/sync/list/$', response_list, name='api_sync_list'),
82 url(r'^api/sync/get/$', response_get, name='api_sync_pull'),
80 url(r'^api/sync/get/$', response_get, name='api_sync_get'),
83 # TODO 'get' request
84
81
85 # Notifications
82 # Notifications
86 url(r'^notifications/(?P<username>\w+)/$', NotificationView.as_view(), name='notifications'),
83 url(r'^notifications/(?P<username>\w+)/$', NotificationView.as_view(), name='notifications'),
87 url(r'^notifications/$', NotificationView.as_view(), name='notifications'),
84 url(r'^notifications/$', NotificationView.as_view(), name='notifications'),
88
85
89 # Post preview
86 # Post preview
90 url(r'^preview/$', PostPreviewView.as_view(), name='preview'),
87 url(r'^preview/$', PostPreviewView.as_view(), name='preview'),
91 url(r'^post_xml/(?P<post_id>\d+)$', get_post_sync_data,
88 url(r'^post_xml/(?P<post_id>\d+)$', get_post_sync_data,
92 name='post_sync_data'),
89 name='post_sync_data'),
93 ]
90 ]
94
91
95 # Search
92 # Search
96 if 'haystack' in neboard.settings.INSTALLED_APPS:
93 if 'haystack' in neboard.settings.INSTALLED_APPS:
97 from boards.views.search import BoardSearchView
94 from boards.views.search import BoardSearchView
98 urlpatterns.append(url(r'^search/$', BoardSearchView.as_view(), name='search'))
95 urlpatterns.append(url(r'^search/$', BoardSearchView.as_view(), name='search'))
99
96
@@ -1,62 +1,62 b''
1 import xml.etree.ElementTree as et
1 import xml.etree.ElementTree as et
2 import xml.dom.minidom
2 import xml.dom.minidom
3
3
4 from django.http import HttpResponse, Http404
4 from django.http import HttpResponse, Http404
5 from boards.models import GlobalId, Post
5 from boards.models import GlobalId, Post
6 from boards.models.post.sync import SyncManager
6 from boards.models.post.sync import SyncManager
7
7
8
8
9 def response_pull(request):
9 def response_list(request):
10 request_xml = request.body
10 request_xml = request.body
11
11
12 if request_xml is None:
12 if request_xml is None or len(request_xml) == 0:
13 return HttpResponse(content='Use the API')
13 return HttpResponse(content='Use the API')
14
14
15 response_xml = SyncManager.generate_response_pull()
15 response_xml = SyncManager.generate_response_list()
16
16
17 return HttpResponse(content=response_xml)
17 return HttpResponse(content=response_xml)
18
18
19
19
20 def response_get(request):
20 def response_get(request):
21 """
21 """
22 Processes a GET request with post ID list and returns the posts XML list.
22 Processes a GET request with post ID list and returns the posts XML list.
23 Request should contain an 'xml' post attribute with the actual request XML.
23 Request should contain an 'xml' post attribute with the actual request XML.
24 """
24 """
25
25
26 request_xml = request.body
26 request_xml = request.body
27
27
28 if request_xml is None:
28 if request_xml is None or len(request_xml) == 0:
29 return HttpResponse(content='Use the API')
29 return HttpResponse(content='Use the API')
30
30
31 posts = []
31 posts = []
32
32
33 root_tag = et.fromstring(request_xml)
33 root_tag = et.fromstring(request_xml)
34 model_tag = root_tag[0]
34 model_tag = root_tag[0]
35 for id_tag in model_tag:
35 for id_tag in model_tag:
36 global_id, exists = GlobalId.from_xml_element(id_tag)
36 global_id, exists = GlobalId.from_xml_element(id_tag)
37 if exists:
37 if exists:
38 posts.append(Post.objects.get(global_id=global_id))
38 posts.append(Post.objects.get(global_id=global_id))
39
39
40 response_xml = SyncManager.generate_response_get(posts)
40 response_xml = SyncManager.generate_response_get(posts)
41
41
42 return HttpResponse(content=response_xml)
42 return HttpResponse(content=response_xml)
43
43
44
44
45 def get_post_sync_data(request, post_id):
45 def get_post_sync_data(request, post_id):
46 try:
46 try:
47 post = Post.objects.get(id=post_id)
47 post = Post.objects.get(id=post_id)
48 except Post.DoesNotExist:
48 except Post.DoesNotExist:
49 raise Http404()
49 raise Http404()
50
50
51 xml_str = SyncManager.generate_response_get([post])
51 xml_str = SyncManager.generate_response_get([post])
52
52
53 xml_repr = xml.dom.minidom.parseString(xml_str)
53 xml_repr = xml.dom.minidom.parseString(xml_str)
54 xml_repr = xml_repr.toprettyxml()
54 xml_repr = xml_repr.toprettyxml()
55
55
56 content = '=Global ID=\n%s\n\n=XML=\n%s' \
56 content = '=Global ID=\n%s\n\n=XML=\n%s' \
57 % (post.global_id, xml_repr)
57 % (post.global_id, xml_repr)
58
58
59 return HttpResponse(
59 return HttpResponse(
60 content_type='text/plain; charset=utf-8',
60 content_type='text/plain; charset=utf-8',
61 content=content,
61 content=content,
62 ) No newline at end of file
62 )
@@ -1,203 +1,203 b''
1 # 0 Title #
1 # 0 Title #
2
2
3 DIP-1 Common protocol description
3 DIP-1 Common protocol description
4
4
5 # 1 Intro #
5 # 1 Intro #
6
6
7 This document describes the Data Interchange Protocol (DIP), designed to
7 This document describes the Data Interchange Protocol (DIP), designed to
8 exchange filtered data that can be stored as a graph structure between
8 exchange filtered data that can be stored as a graph structure between
9 network nodes.
9 network nodes.
10
10
11 # 2 Purpose #
11 # 2 Purpose #
12
12
13 This protocol will be used to share the models (originally imageboard posts)
13 This protocol will be used to share the models (originally imageboard posts)
14 across multiple servers. The main differnce of this protocol is that the node
14 across multiple servers. The main differnce of this protocol is that the node
15 can specify what models it wants to get and from whom. The nodes can get
15 can specify what models it wants to get and from whom. The nodes can get
16 models from a specific server, or from all except some specific servers. Also
16 models from a specific server, or from all except some specific servers. Also
17 the models can be filtered by timestamps or tags.
17 the models can be filtered by timestamps or tags.
18
18
19 # 3 Protocol description #
19 # 3 Protocol description #
20
20
21 The node requests other node's changes list since some time (since epoch if
21 The node requests other node's changes list since some time (since epoch if
22 this is the start). The other node sends a list of post ids or posts in the
22 this is the start). The other node sends a list of post ids or posts in the
23 XML format.
23 XML format.
24
24
25 Protocol version is the version of the sync api. Model version is the version
25 Protocol version is the version of the sync api. Model version is the version
26 of data models. If at least one of them is different, the sync cannot be
26 of data models. If at least one of them is different, the sync cannot be
27 performed.
27 performed.
28
28
29 The node signs the data with its keys. The receiving node saves the key at the
29 The node signs the data with its keys. The receiving node saves the key at the
30 first sync and checks it every time. If the key has changed, the info won't be
30 first sync and checks it every time. If the key has changed, the info won't be
31 saved from the node (or the node id must be changed). A model can be signed
31 saved from the node (or the node id must be changed). A model can be signed
32 with several keys but at least one of them must be the same as in the global
32 with several keys but at least one of them must be the same as in the global
33 ID to verify the sender.
33 ID to verify the sender.
34
34
35 Each node can have several keys. Nodes can have shared keys to serve as a pool
35 Each node can have several keys. Nodes can have shared keys to serve as a pool
36 (several nodes with the same key).
36 (several nodes with the same key).
37
37
38 Each post has an ID in the unique format: key-type::key::local-id
38 Each post has an ID in the unique format: key-type::key::local-id
39
39
40 All requests pass a request type, protocol and model versions, and a list of
40 All requests pass a request type, protocol and model versions, and a list of
41 optional arguments used for filtering.
41 optional arguments used for filtering.
42
42
43 Each request has its own version. Version consists of 2 numbers: first is
43 Each request has its own version. Version consists of 2 numbers: first is
44 incompatible version (1.3 and 2.0 are not compatible and must not be in sync)
44 incompatible version (1.3 and 2.0 are not compatible and must not be in sync)
45 and the second one is minor and compatible (for example, new optional field
45 and the second one is minor and compatible (for example, new optional field
46 is added which will be igroned by those who don't support it yet).
46 is added which will be igroned by those who don't support it yet).
47
47
48 Post edits and reflinks are not saved to the sync model. The replied post ID
48 Post edits and reflinks are not saved to the sync model. The replied post ID
49 can be got from the post text, and reflinks can be computed when loading
49 can be got from the post text, and reflinks can be computed when loading
50 posts. The edit time is not saved because a foreign post can be 'edited' (new
50 posts. The edit time is not saved because a foreign post can be 'edited' (new
51 replies are added) but the signature must not change (so we can't update the
51 replies are added) but the signature must not change (so we can't update the
52 content). The inner posts can be edited, and the signature will change then
52 content). The inner posts can be edited, and the signature will change then
53 but the local-id won't, so the other node can detect that and replace the post
53 but the local-id won't, so the other node can detect that and replace the post
54 instead of adding a new one.
54 instead of adding a new one.
55
55
56 ## 3.1 Requests ##
56 ## 3.1 Requests ##
57
57
58 There is no constraint on how the server should calculate the request. The
58 There is no constraint on how the server should calculate the request. The
59 server can return any information by any filter and the requesting node is
59 server can return any information by any filter and the requesting node is
60 responsible for validating it.
60 responsible for validating it.
61
61
62 The server is required to return the status of request. See 3.2 for details.
62 The server is required to return the status of request. See 3.2 for details.
63
63
64 ### 3.1.1 pull ###
64 ### 3.1.1 list ###
65
65
66 "pull" request gets the desired model id list by the given filter (e.g. thread, tags,
66 "list" request gets the desired model id list by the given filter (e.g. thread, tags,
67 author)
67 author)
68
68
69 Sample request is as follows:
69 Sample request is as follows:
70
70
71 <?xml version="1.1" encoding="UTF-8" ?>
71 <?xml version="1.1" encoding="UTF-8" ?>
72 <request version="1.0" type="pull">
72 <request version="1.0" type="list">
73 <model version="1.0" name="post">
73 <model version="1.0" name="post">
74 <timestamp_from>0</timestamp_from>
74 <timestamp_from>0</timestamp_from>
75 <timestamp_to>0</timestamp_to>
75 <timestamp_to>0</timestamp_to>
76 <tags>
76 <tags>
77 <tag>tag1</tag>
77 <tag>tag1</tag>
78 </tags>
78 </tags>
79 <sender>
79 <sender>
80 <allow>
80 <allow>
81 <key>abcehy3h9t</key>
81 <key>abcehy3h9t</key>
82 <key>ehoehyoe</key>
82 <key>ehoehyoe</key>
83 </allow>
83 </allow>
84 <!-- There can be only allow block (all other are denied) or deny block (all other are allowed) -->
84 <!-- There can be only allow block (all other are denied) or deny block (all other are allowed) -->
85 </sender>
85 </sender>
86 </model>
86 </model>
87 </request>
87 </request>
88
88
89 Under the <model> tag there are filters. Filters for the "post" model can
89 Under the <model> tag there are filters. Filters for the "post" model can
90 be found in DIP-2.
90 be found in DIP-2.
91
91
92 Sample response:
92 Sample response:
93
93
94 <?xml version="1.1" encoding="UTF-8" ?>
94 <?xml version="1.1" encoding="UTF-8" ?>
95 <response>
95 <response>
96 <status>success</status>
96 <status>success</status>
97 <models>
97 <models>
98 <id key="id1" type="ecdsa" local-id="1" />
98 <id key="id1" type="ecdsa" local-id="1" />
99 <id key="id1" type="ecdsa" local-id="2" />
99 <id key="id1" type="ecdsa" local-id="2" />
100 <id key="id2" type="ecdsa" local-id="1" />
100 <id key="id2" type="ecdsa" local-id="1" />
101 <id key="id2" type="ecdsa" local-id="5" />
101 <id key="id2" type="ecdsa" local-id="5" />
102 </models>
102 </models>
103 </response>
103 </response>
104
104
105 ### 3.1.2 get ###
105 ### 3.1.2 get ###
106
106
107 "get" gets models by id list.
107 "get" gets models by id list.
108
108
109 Sample request:
109 Sample request:
110
110
111 <?xml version="1.1" encoding="UTF-8" ?>
111 <?xml version="1.1" encoding="UTF-8" ?>
112 <request version="1.0" type="get">
112 <request version="1.0" type="get">
113 <model version="1.0" name="post">
113 <model version="1.0" name="post">
114 <id key="id1" type="ecdsa" local-id="1" />
114 <id key="id1" type="ecdsa" local-id="1" />
115 <id key="id1" type="ecdsa" local-id="2" />
115 <id key="id1" type="ecdsa" local-id="2" />
116 </model>
116 </model>
117 </request>
117 </request>
118
118
119 Id consists of a key, key type and local id. This key is used for signing and
119 Id consists of a key, key type and local id. This key is used for signing and
120 validating of data in the model content.
120 validating of data in the model content.
121
121
122 Sample response:
122 Sample response:
123
123
124 <?xml version="1.1" encoding="UTF-8" ?>
124 <?xml version="1.1" encoding="UTF-8" ?>
125 <response>
125 <response>
126 <!--
126 <!--
127 Valid statuses are 'success' and 'error'.
127 Valid statuses are 'success' and 'error'.
128 -->
128 -->
129 <status>success</status>
129 <status>success</status>
130 <models>
130 <models>
131 <model name="post">
131 <model name="post">
132 <!--
132 <!--
133 Content tag is the data that is signed by signatures and must
133 Content tag is the data that is signed by signatures and must
134 not be changed for the post from other node.
134 not be changed for the post from other node.
135 -->
135 -->
136 <content>
136 <content>
137 <id key="id1" type="ecdsa" local-id="1" />
137 <id key="id1" type="ecdsa" local-id="1" />
138 <title>13</title>
138 <title>13</title>
139 <text>Thirteen</text>
139 <text>Thirteen</text>
140 <thread><id key="id1" type="ecdsa" local-id="2" /></thread>
140 <thread><id key="id1" type="ecdsa" local-id="2" /></thread>
141 <pub-time>12</pub-time>
141 <pub-time>12</pub-time>
142 <!--
142 <!--
143 Images are saved as attachments and included in the
143 Images are saved as attachments and included in the
144 signature.
144 signature.
145 -->
145 -->
146 <attachments>
146 <attachments>
147 <attachment mimetype="image/png" id-type="md5">TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5I</attachment>
147 <attachment mimetype="image/png" id-type="md5">TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5I</attachment>
148 </attachments>
148 </attachments>
149 </content>
149 </content>
150 <!--
150 <!--
151 There can be several signatures for one model. At least one
151 There can be several signatures for one model. At least one
152 signature must be made with the key used in global ID.
152 signature must be made with the key used in global ID.
153 -->
153 -->
154 <signatures>
154 <signatures>
155 <signature key="id1" type="ecdsa" value="dhefhtreh" />
155 <signature key="id1" type="ecdsa" value="dhefhtreh" />
156 <signature key="id45" type="ecdsa" value="dsgfgdhefhtreh" />
156 <signature key="id45" type="ecdsa" value="dsgfgdhefhtreh" />
157 </signatures>
157 </signatures>
158 <attachment-refs>
158 <attachment-refs>
159 <attachment-ref ref="TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0"
159 <attachment-ref ref="TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0"
160 url="/media/images/12345.png" />
160 url="/media/images/12345.png" />
161 </attachment-refs>
161 </attachment-refs>
162 </model>
162 </model>
163 <model name="post">
163 <model name="post">
164 <content>
164 <content>
165 <id key="id1" type="ecdsa" local-id="id2" />
165 <id key="id1" type="ecdsa" local-id="id2" />
166 <title>13</title>
166 <title>13</title>
167 <text>Thirteen</text>
167 <text>Thirteen</text>
168 <pub-time>12</pub-time>
168 <pub-time>12</pub-time>
169 <edit-time>13</edit-time>
169 <edit-time>13</edit-time>
170 <tags>
170 <tags>
171 <tag>tag1</tag>
171 <tag>tag1</tag>
172 </tags>
172 </tags>
173 </content>
173 </content>
174 <signatures>
174 <signatures>
175 <signature key="id2" type="ecdsa" value="dehdfh" />
175 <signature key="id2" type="ecdsa" value="dehdfh" />
176 </signatures>
176 </signatures>
177 </model>
177 </model>
178 </models>
178 </models>
179 </response>
179 </response>
180
180
181 ### 3.1.3 put ###
181 ### 3.1.3 put ###
182
182
183 "put" gives a model to the given node (you have no guarantee the node takes
183 "put" gives a model to the given node (you have no guarantee the node takes
184 it, consider you are just advising the node to take your post). This request
184 it, consider you are just advising the node to take your post). This request
185 type is useful in pool where all the nodes try to duplicate all of their data
185 type is useful in pool where all the nodes try to duplicate all of their data
186 across the pool.
186 across the pool.
187
187
188 ## 3.2 Responses ##
188 ## 3.2 Responses ##
189
189
190 ### 3.2.1 "not supported" ###
190 ### 3.2.1 "not supported" ###
191
191
192 If the request if completely not supported, a "not supported" status will be
192 If the request if completely not supported, a "not supported" status will be
193 returned.
193 returned.
194
194
195 ### 3.2.2 "success" ###
195 ### 3.2.2 "success" ###
196
196
197 "success" status means the request was processed and the result is returned.
197 "success" status means the request was processed and the result is returned.
198
198
199 ### 3.2.3 "error" ###
199 ### 3.2.3 "error" ###
200
200
201 If the server knows for sure that the operation cannot be processed, it sends
201 If the server knows for sure that the operation cannot be processed, it sends
202 the "error" status. Additional tags describing the error may be <description>
202 the "error" status. Additional tags describing the error may be <description>
203 and <stack>.
203 and <stack>.
General Comments 0
You need to be logged in to leave comments. Login now