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