##// END OF EJS Templates
Added version to post content
neko259 -
r1569:a4ed3791 default
parent child Browse files
Show More
@@ -0,0 +1,20 b''
1 # -*- coding: utf-8 -*-
2 # Generated by Django 1.9.5 on 2016-05-11 09:23
3 from __future__ import unicode_literals
4
5 from django.db import migrations, models
6
7
8 class Migration(migrations.Migration):
9
10 dependencies = [
11 ('boards', '0044_globalid_content'),
12 ]
13
14 operations = [
15 migrations.AddField(
16 model_name='post',
17 name='version',
18 field=models.IntegerField(default=1),
19 ),
20 ]
@@ -1,136 +1,151 b''
1 1 from django.contrib import admin
2 2 from django.utils.translation import ugettext_lazy as _
3 3 from django.core.urlresolvers import reverse
4 from django.db.models import F
4 5 from boards.models import Post, Tag, Ban, Thread, Banner, PostImage, KeyPair, GlobalId
5 6
6 7
7 8 @admin.register(Post)
8 9 class PostAdmin(admin.ModelAdmin):
9 10
10 11 list_display = ('id', 'title', 'text', 'poster_ip', 'linked_images', 'linked_global_id')
11 12 list_filter = ('pub_time',)
12 13 search_fields = ('id', 'title', 'text', 'poster_ip')
13 14 exclude = ('referenced_posts', 'refmap', 'images', 'global_id')
14 15 readonly_fields = ('poster_ip', 'threads', 'thread', 'linked_images',
15 'attachments', 'uid', 'url', 'pub_time', 'opening', 'linked_global_id')
16 'attachments', 'uid', 'url', 'pub_time', 'opening', 'linked_global_id',
17 'version')
16 18
17 19 def ban_poster(self, request, queryset):
18 20 bans = 0
19 21 for post in queryset:
20 22 poster_ip = post.poster_ip
21 23 ban, created = Ban.objects.get_or_create(ip=poster_ip)
22 24 if created:
23 25 bans += 1
24 26 self.message_user(request, _('{} posters were banned').format(bans))
25 27
26 28 def ban_with_hiding(self, request, queryset):
27 29 bans = 0
28 30 hidden = 0
29 31 for post in queryset:
30 32 poster_ip = post.poster_ip
31 33 ban, created = Ban.objects.get_or_create(ip=poster_ip)
32 34 if created:
33 35 bans += 1
34 36 posts = Post.objects.filter(poster_ip=poster_ip, id__gte=post.id)
35 37 hidden += posts.count()
36 38 posts.update(hidden=True)
37 39 self.message_user(request, _('{} posters were banned, {} messages were hidden').format(bans, hidden))
38 40
39 41 def linked_images(self, obj: Post):
40 42 images = obj.images.all()
41 43 image_urls = ['<a href="{}"><img src="{}" /></a>'.format(
42 44 reverse('admin:%s_%s_change' % (image._meta.app_label,
43 45 image._meta.model_name),
44 46 args=[image.id]), image.image.url_200x150) for image in images]
45 47 return ', '.join(image_urls)
46 48 linked_images.allow_tags = True
47 49
48 50 def linked_global_id(self, obj: Post):
49 51 global_id = obj.global_id
50 52 if global_id is not None:
51 53 return '<a href="{}">{}</a>'.format(
52 54 reverse('admin:%s_%s_change' % (global_id._meta.app_label,
53 55 global_id._meta.model_name),
54 56 args=[global_id.id]), str(global_id))
55 57 linked_global_id.allow_tags = True
56 58
59 def save_model(self, request, obj, form, change):
60 obj.increment_version()
61 obj.save()
62 obj.clear_cache()
63
57 64 actions = ['ban_poster', 'ban_with_hiding']
58 65
59 66
60 67 @admin.register(Tag)
61 68 class TagAdmin(admin.ModelAdmin):
62 69
63 70 def thread_count(self, obj: Tag) -> int:
64 71 return obj.get_thread_count()
65 72
66 73 def display_children(self, obj: Tag):
67 74 return ', '.join([str(child) for child in obj.get_children().all()])
68 75
69 76 def save_model(self, request, obj, form, change):
70 77 super().save_model(request, obj, form, change)
71 78 for thread in obj.get_threads().all():
72 79 thread.refresh_tags()
73 80 list_display = ('name', 'thread_count', 'display_children')
74 81 search_fields = ('name',)
75 82
76 83
77 84 @admin.register(Thread)
78 85 class ThreadAdmin(admin.ModelAdmin):
79 86
80 87 def title(self, obj: Thread) -> str:
81 88 return obj.get_opening_post().get_title()
82 89
83 90 def reply_count(self, obj: Thread) -> int:
84 91 return obj.get_reply_count()
85 92
86 93 def ip(self, obj: Thread):
87 94 return obj.get_opening_post().poster_ip
88 95
89 96 def display_tags(self, obj: Thread):
90 97 return ', '.join([str(tag) for tag in obj.get_tags().all()])
91 98
92 99 def op(self, obj: Thread):
93 100 return obj.get_opening_post_id()
94 101
95 102 # Save parent tags when editing tags
96 103 def save_related(self, request, form, formsets, change):
97 104 super().save_related(request, form, formsets, change)
98 105 form.instance.refresh_tags()
106
107 def save_model(self, request, obj, form, change):
108 op = obj.get_opening_post()
109 op.increment_version()
110 op.save(update_fields=['version'])
111 obj.save()
112 op.clear_cache()
113
99 114 list_display = ('id', 'op', 'title', 'reply_count', 'status', 'ip',
100 115 'display_tags')
101 116 list_filter = ('bump_time', 'status')
102 117 search_fields = ('id', 'title')
103 118 filter_horizontal = ('tags',)
104 119
105 120
106 121 @admin.register(KeyPair)
107 122 class KeyPairAdmin(admin.ModelAdmin):
108 123 list_display = ('public_key', 'primary')
109 124 list_filter = ('primary',)
110 125 search_fields = ('public_key',)
111 126
112 127
113 128 @admin.register(Ban)
114 129 class BanAdmin(admin.ModelAdmin):
115 130 list_display = ('ip', 'can_read')
116 131 list_filter = ('can_read',)
117 132 search_fields = ('ip',)
118 133
119 134
120 135 @admin.register(Banner)
121 136 class BannerAdmin(admin.ModelAdmin):
122 137 list_display = ('title', 'text')
123 138
124 139
125 140 @admin.register(PostImage)
126 141 class PostImageAdmin(admin.ModelAdmin):
127 142 search_fields = ('alias',)
128 143
129 144
130 145 @admin.register(GlobalId)
131 146 class GlobalIdAdmin(admin.ModelAdmin):
132 147 def is_linked(self, obj):
133 148 return Post.objects.filter(global_id=obj).exists()
134 149
135 150 list_display = ('__str__', 'is_linked',)
136 151 readonly_fields = ('content',)
@@ -1,381 +1,393 b''
1 1 import uuid
2 2
3 3 import re
4 4 from boards import settings
5 5 from boards.abstracts.tripcode import Tripcode
6 6 from boards.models import PostImage, Attachment, KeyPair, GlobalId
7 7 from boards.models.base import Viewable
8 8 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
9 9 from boards.models.post.manager import PostManager
10 10 from boards.utils import datetime_to_epoch
11 11 from django.core.exceptions import ObjectDoesNotExist
12 12 from django.core.urlresolvers import reverse
13 13 from django.db import models
14 from django.db.models import TextField, QuerySet
14 from django.db.models import TextField, QuerySet, F
15 15 from django.template.defaultfilters import truncatewords, striptags
16 16 from django.template.loader import render_to_string
17 17
18 18 CSS_CLS_HIDDEN_POST = 'hidden_post'
19 19 CSS_CLS_DEAD_POST = 'dead_post'
20 20 CSS_CLS_ARCHIVE_POST = 'archive_post'
21 21 CSS_CLS_POST = 'post'
22 22 CSS_CLS_MONOCHROME = 'monochrome'
23 23
24 24 TITLE_MAX_WORDS = 10
25 25
26 26 APP_LABEL_BOARDS = 'boards'
27 27
28 28 BAN_REASON_AUTO = 'Auto'
29 29
30 30 IMAGE_THUMB_SIZE = (200, 150)
31 31
32 32 TITLE_MAX_LENGTH = 200
33 33
34 34 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
35 35 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
36 36 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
37 37 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
38 38
39 39 PARAMETER_TRUNCATED = 'truncated'
40 40 PARAMETER_TAG = 'tag'
41 41 PARAMETER_OFFSET = 'offset'
42 42 PARAMETER_DIFF_TYPE = 'type'
43 43 PARAMETER_CSS_CLASS = 'css_class'
44 44 PARAMETER_THREAD = 'thread'
45 45 PARAMETER_IS_OPENING = 'is_opening'
46 46 PARAMETER_POST = 'post'
47 47 PARAMETER_OP_ID = 'opening_post_id'
48 48 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
49 49 PARAMETER_REPLY_LINK = 'reply_link'
50 50 PARAMETER_NEED_OP_DATA = 'need_op_data'
51 51
52 52 POST_VIEW_PARAMS = (
53 53 'need_op_data',
54 54 'reply_link',
55 55 'need_open_link',
56 56 'truncated',
57 57 'mode_tree',
58 58 'perms',
59 59 'tree_depth',
60 60 )
61 61
62 62
63 63 class Post(models.Model, Viewable):
64 64 """A post is a message."""
65 65
66 66 objects = PostManager()
67 67
68 68 class Meta:
69 69 app_label = APP_LABEL_BOARDS
70 70 ordering = ('id',)
71 71
72 72 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
73 73 pub_time = models.DateTimeField()
74 74 text = TextField(blank=True, null=True)
75 75 _text_rendered = TextField(blank=True, null=True, editable=False)
76 76
77 77 images = models.ManyToManyField(PostImage, null=True, blank=True,
78 78 related_name='post_images', db_index=True)
79 79 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
80 80 related_name='attachment_posts')
81 81
82 82 poster_ip = models.GenericIPAddressField()
83 83
84 84 # TODO This field can be removed cause UID is used for update now
85 85 last_edit_time = models.DateTimeField()
86 86
87 87 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
88 88 null=True,
89 89 blank=True, related_name='refposts',
90 90 db_index=True)
91 91 refmap = models.TextField(null=True, blank=True)
92 92 threads = models.ManyToManyField('Thread', db_index=True,
93 93 related_name='multi_replies')
94 94 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
95 95
96 96 url = models.TextField()
97 97 uid = models.TextField(db_index=True)
98 98
99 99 # Global ID with author key. If the message was downloaded from another
100 100 # server, this indicates the server.
101 101 global_id = models.OneToOneField(GlobalId, null=True, blank=True,
102 102 on_delete=models.CASCADE)
103 103
104 104 tripcode = models.CharField(max_length=50, blank=True, default='')
105 105 opening = models.BooleanField(db_index=True)
106 106 hidden = models.BooleanField(default=False)
107 version = models.IntegerField(default=1)
107 108
108 109 def __str__(self):
109 110 return 'P#{}/{}'.format(self.id, self.get_title())
110 111
111 112 def get_title(self) -> str:
112 113 return self.title
113 114
114 115 def get_title_or_text(self):
115 116 title = self.get_title()
116 117 if not title:
117 118 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
118 119
119 120 return title
120 121
121 122 def build_refmap(self) -> None:
122 123 """
123 124 Builds a replies map string from replies list. This is a cache to stop
124 125 the server from recalculating the map on every post show.
125 126 """
126 127
127 128 post_urls = [refpost.get_link_view()
128 129 for refpost in self.referenced_posts.all()]
129 130
130 131 self.refmap = ', '.join(post_urls)
131 132
132 133 def is_referenced(self) -> bool:
133 134 return self.refmap and len(self.refmap) > 0
134 135
135 136 def is_opening(self) -> bool:
136 137 """
137 138 Checks if this is an opening post or just a reply.
138 139 """
139 140
140 141 return self.opening
141 142
142 143 def get_absolute_url(self, thread=None):
143 144 url = None
144 145
145 146 if thread is None:
146 147 thread = self.get_thread()
147 148
148 149 # Url is cached only for the "main" thread. When getting url
149 150 # for other threads, do it manually.
150 151 if self.url:
151 152 url = self.url
152 153
153 154 if url is None:
154 155 opening = self.is_opening()
155 156 opening_id = self.id if opening else thread.get_opening_post_id()
156 157 url = reverse('thread', kwargs={'post_id': opening_id})
157 158 if not opening:
158 159 url += '#' + str(self.id)
159 160
160 161 return url
161 162
162 163 def get_thread(self):
163 164 return self.thread
164 165
165 166 def get_thread_id(self):
166 167 return self.thread_id
167 168
168 169 def get_threads(self) -> QuerySet:
169 170 """
170 171 Gets post's thread.
171 172 """
172 173
173 174 return self.threads
174 175
175 176 def _get_cache_key(self):
176 177 return [datetime_to_epoch(self.last_edit_time)]
177 178
178 179 def get_view(self, *args, **kwargs) -> str:
179 180 """
180 181 Renders post's HTML view. Some of the post params can be passed over
181 182 kwargs for the means of caching (if we view the thread, some params
182 183 are same for every post and don't need to be computed over and over.
183 184 """
184 185
185 186 thread = self.get_thread()
186 187
187 188 css_classes = [CSS_CLS_POST]
188 189 if thread.is_archived():
189 190 css_classes.append(CSS_CLS_ARCHIVE_POST)
190 191 elif not thread.can_bump():
191 192 css_classes.append(CSS_CLS_DEAD_POST)
192 193 if self.is_hidden():
193 194 css_classes.append(CSS_CLS_HIDDEN_POST)
194 195 if thread.is_monochrome():
195 196 css_classes.append(CSS_CLS_MONOCHROME)
196 197
197 198 params = dict()
198 199 for param in POST_VIEW_PARAMS:
199 200 if param in kwargs:
200 201 params[param] = kwargs[param]
201 202
202 203 params.update({
203 204 PARAMETER_POST: self,
204 205 PARAMETER_IS_OPENING: self.is_opening(),
205 206 PARAMETER_THREAD: thread,
206 207 PARAMETER_CSS_CLASS: ' '.join(css_classes),
207 208 })
208 209
209 210 return render_to_string('boards/post.html', params)
210 211
211 212 def get_search_view(self, *args, **kwargs):
212 213 return self.get_view(need_op_data=True, *args, **kwargs)
213 214
214 215 def get_first_image(self) -> PostImage:
215 216 return self.images.earliest('id')
216 217
217 218 def set_global_id(self, key_pair=None):
218 219 """
219 220 Sets global id based on the given key pair. If no key pair is given,
220 221 default one is used.
221 222 """
222 223
223 224 if key_pair:
224 225 key = key_pair
225 226 else:
226 227 try:
227 228 key = KeyPair.objects.get(primary=True)
228 229 except KeyPair.DoesNotExist:
229 230 # Do not update the global id because there is no key defined
230 231 return
231 232 global_id = GlobalId(key_type=key.key_type,
232 233 key=key.public_key,
233 234 local_id=self.id)
234 235 global_id.save()
235 236
236 237 self.global_id = global_id
237 238
238 239 self.save(update_fields=['global_id'])
239 240
240 241 def get_pub_time_str(self):
241 242 return str(self.pub_time)
242 243
243 244 def get_replied_ids(self):
244 245 """
245 246 Gets ID list of the posts that this post replies.
246 247 """
247 248
248 249 raw_text = self.get_raw_text()
249 250
250 251 local_replied = REGEX_REPLY.findall(raw_text)
251 252 global_replied = []
252 253 for match in REGEX_GLOBAL_REPLY.findall(raw_text):
253 254 key_type = match[0]
254 255 key = match[1]
255 256 local_id = match[2]
256 257
257 258 try:
258 259 global_id = GlobalId.objects.get(key_type=key_type,
259 260 key=key, local_id=local_id)
260 261 for post in Post.objects.filter(global_id=global_id).only('id'):
261 262 global_replied.append(post.id)
262 263 except GlobalId.DoesNotExist:
263 264 pass
264 265 return local_replied + global_replied
265 266
266 267 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
267 268 include_last_update=False) -> str:
268 269 """
269 270 Gets post HTML or JSON data that can be rendered on a page or used by
270 271 API.
271 272 """
272 273
273 274 return get_exporter(format_type).export(self, request,
274 275 include_last_update)
275 276
276 277 def notify_clients(self, recursive=True):
277 278 """
278 279 Sends post HTML data to the thread web socket.
279 280 """
280 281
281 282 if not settings.get_bool('External', 'WebsocketsEnabled'):
282 283 return
283 284
284 285 thread_ids = list()
285 286 for thread in self.get_threads().all():
286 287 thread_ids.append(thread.id)
287 288
288 289 thread.notify_clients()
289 290
290 291 if recursive:
291 292 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
292 293 post_id = reply_number.group(1)
293 294
294 295 try:
295 296 ref_post = Post.objects.get(id=post_id)
296 297
297 298 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
298 299 # If post is in this thread, its thread was already notified.
299 300 # Otherwise, notify its thread separately.
300 301 ref_post.notify_clients(recursive=False)
301 302 except ObjectDoesNotExist:
302 303 pass
303 304
304 305 def build_url(self):
305 306 self.url = self.get_absolute_url()
306 307 self.save(update_fields=['url'])
307 308
308 309 def save(self, force_insert=False, force_update=False, using=None,
309 310 update_fields=None):
310 311 new_post = self.id is None
311 312
312 313 self.uid = str(uuid.uuid4())
313 314 if update_fields is not None and 'uid' not in update_fields:
314 315 update_fields += ['uid']
315 316
316 317 if not new_post:
317 318 for thread in self.get_threads().all():
318 319 thread.last_edit_time = self.last_edit_time
319 320
320 321 thread.save(update_fields=['last_edit_time', 'status'])
321 322
322 323 super().save(force_insert, force_update, using, update_fields)
323 324
324 325 if self.url is None:
325 326 self.build_url()
326 327
327 328 def get_text(self) -> str:
328 329 return self._text_rendered
329 330
330 331 def get_raw_text(self) -> str:
331 332 return self.text
332 333
333 334 def get_sync_text(self) -> str:
334 335 """
335 336 Returns text applicable for sync. It has absolute post reflinks.
336 337 """
337 338
338 339 replacements = dict()
339 340 for post_id in REGEX_REPLY.findall(self.get_raw_text()):
340 341 absolute_post_id = str(Post.objects.get(id=post_id).global_id)
341 342 replacements[post_id] = absolute_post_id
342 343
343 344 text = self.get_raw_text() or ''
344 345 for key in replacements:
345 346 text = text.replace('[post]{}[/post]'.format(key),
346 347 '[post]{}[/post]'.format(replacements[key]))
347 348 text = text.replace('\r\n', '\n').replace('\r', '\n')
348 349
349 350 return text
350 351
351 352 def connect_threads(self, opening_posts):
352 353 for opening_post in opening_posts:
353 354 threads = opening_post.get_threads().all()
354 355 for thread in threads:
355 356 if thread.can_bump():
356 357 thread.update_bump_status()
357 358
358 359 thread.last_edit_time = self.last_edit_time
359 360 thread.save(update_fields=['last_edit_time', 'status'])
360 361 self.threads.add(opening_post.get_thread())
361 362
362 363 def get_tripcode(self):
363 364 if self.tripcode:
364 365 return Tripcode(self.tripcode)
365 366
366 367 def get_link_view(self):
367 368 """
368 369 Gets view of a reflink to the post.
369 370 """
370 371 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
371 372 self.id)
372 373 if self.is_opening():
373 374 result = '<b>{}</b>'.format(result)
374 375
375 376 return result
376 377
377 378 def is_hidden(self) -> bool:
378 379 return self.hidden
379 380
380 381 def set_hidden(self, hidden):
381 382 self.hidden = hidden
383
384 def increment_version(self):
385 self.version = F('version') + 1
386
387 def clear_cache(self):
388 global_id = self.global_id
389 if global_id is not None and global_id.is_local()\
390 and global_id.content is not None:
391 global_id.content = None
392 global_id.save()
393 global_id.signature_set.all().delete()
@@ -1,284 +1,287 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 TAG_VERSION = 'version'
35 36
36 37 TYPE_GET = 'get'
37 38
38 39 ATTR_VERSION = 'version'
39 40 ATTR_TYPE = 'type'
40 41 ATTR_NAME = 'name'
41 42 ATTR_VALUE = 'value'
42 43 ATTR_MIMETYPE = 'mimetype'
43 44 ATTR_KEY = 'key'
44 45 ATTR_REF = 'ref'
45 46 ATTR_URL = 'url'
46 47 ATTR_ID_TYPE = 'id-type'
47 48
48 49 ID_TYPE_MD5 = 'md5'
49 50
50 51 STATUS_SUCCESS = 'success'
51 52
52 53
53 54 class SyncException(Exception):
54 55 pass
55 56
56 57
57 58 class SyncManager:
58 59 @staticmethod
59 60 def generate_response_get(model_list: list):
60 61 response = et.Element(TAG_RESPONSE)
61 62
62 63 status = et.SubElement(response, TAG_STATUS)
63 64 status.text = STATUS_SUCCESS
64 65
65 66 models = et.SubElement(response, TAG_MODELS)
66 67
67 68 for post in model_list:
68 69 model = et.SubElement(models, TAG_MODEL)
69 70 model.set(ATTR_NAME, 'post')
70 71
71 72 global_id = post.global_id
72 73
73 74 images = post.images.all()
74 75 attachments = post.attachments.all()
75 76 if global_id.content:
76 77 model.append(et.fromstring(global_id.content))
77 78 if len(images) > 0 or len(attachments) > 0:
78 79 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
79 80 for image in images:
80 81 SyncManager._attachment_to_xml(
81 82 None, attachment_refs, image.image.file,
82 83 image.hash, image.image.url)
83 84 for file in attachments:
84 85 SyncManager._attachment_to_xml(
85 86 None, attachment_refs, file.file.file,
86 87 file.hash, file.file.url)
87 88 else:
88 89 content_tag = et.SubElement(model, TAG_CONTENT)
89 90
90 91 tag_id = et.SubElement(content_tag, TAG_ID)
91 92 global_id.to_xml_element(tag_id)
92 93
93 94 title = et.SubElement(content_tag, TAG_TITLE)
94 95 title.text = post.title
95 96
96 97 text = et.SubElement(content_tag, TAG_TEXT)
97 98 text.text = post.get_sync_text()
98 99
99 100 thread = post.get_thread()
100 101 if post.is_opening():
101 102 tag_tags = et.SubElement(content_tag, TAG_TAGS)
102 103 for tag in thread.get_tags():
103 104 tag_tag = et.SubElement(tag_tags, TAG_TAG)
104 105 tag_tag.text = tag.name
105 106 else:
106 107 tag_thread = et.SubElement(content_tag, TAG_THREAD)
107 108 thread_id = et.SubElement(tag_thread, TAG_ID)
108 109 thread.get_opening_post().global_id.to_xml_element(thread_id)
109 110
110 111 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
111 112 pub_time.text = str(post.get_pub_time_str())
112 113
113 114 if post.tripcode:
114 115 tripcode = et.SubElement(content_tag, TAG_TRIPCODE)
115 116 tripcode.text = post.tripcode
116 117
117 118 if len(images) > 0 or len(attachments) > 0:
118 119 attachments_tag = et.SubElement(content_tag, TAG_ATTACHMENTS)
119 120 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
120 121
121 122 for image in images:
122 123 SyncManager._attachment_to_xml(
123 124 attachments_tag, attachment_refs, image.image.file,
124 125 image.hash, image.image.url)
125 126 for file in attachments:
126 127 SyncManager._attachment_to_xml(
127 128 attachments_tag, attachment_refs, file.file.file,
128 129 file.hash, file.file.url)
130 version_tag = et.SubElement(content_tag, TAG_VERSION)
131 version_tag.text = str(post.version)
129 132
130 133 global_id.content = et.tostring(content_tag, ENCODING_UNICODE)
131 134 global_id.save()
132 135
133 136 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
134 137 post_signatures = global_id.signature_set.all()
135 138 if post_signatures:
136 139 signatures = post_signatures
137 140 else:
138 141 key = KeyPair.objects.get(public_key=global_id.key)
139 142 signature = Signature(
140 143 key_type=key.key_type,
141 144 key=key.public_key,
142 145 signature=key.sign(global_id.content),
143 146 global_id=global_id,
144 147 )
145 148 signature.save()
146 149 signatures = [signature]
147 150 for signature in signatures:
148 151 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
149 152 signature_tag.set(ATTR_TYPE, signature.key_type)
150 153 signature_tag.set(ATTR_VALUE, signature.signature)
151 154 signature_tag.set(ATTR_KEY, signature.key)
152 155
153 156 return et.tostring(response, ENCODING_UNICODE)
154 157
155 158 @staticmethod
156 159 @transaction.atomic
157 160 def parse_response_get(response_xml, hostname):
158 161 tag_root = et.fromstring(response_xml)
159 162 tag_status = tag_root.find(TAG_STATUS)
160 163 if STATUS_SUCCESS == tag_status.text:
161 164 tag_models = tag_root.find(TAG_MODELS)
162 165 for tag_model in tag_models:
163 166 tag_content = tag_model.find(TAG_CONTENT)
164 167
165 168 content_str = et.tostring(tag_content, ENCODING_UNICODE)
166 169 signatures = SyncManager._verify_model(content_str, tag_model)
167 170
168 171 tag_id = tag_content.find(TAG_ID)
169 172 global_id, exists = GlobalId.from_xml_element(tag_id)
170 173
171 174 if exists:
172 175 print('Post with same ID already exists')
173 176 else:
174 177 global_id.content = content_str
175 178 global_id.save()
176 179 for signature in signatures:
177 180 signature.global_id = global_id
178 181 signature.save()
179 182
180 183 title = tag_content.find(TAG_TITLE).text or ''
181 184 text = tag_content.find(TAG_TEXT).text or ''
182 185 pub_time = tag_content.find(TAG_PUB_TIME).text
183 186 tripcode_tag = tag_content.find(TAG_TRIPCODE)
184 187 if tripcode_tag is not None:
185 188 tripcode = tripcode_tag.text or ''
186 189 else:
187 190 tripcode = ''
188 191
189 192 thread = tag_content.find(TAG_THREAD)
190 193 tags = []
191 194 if thread:
192 195 thread_id = thread.find(TAG_ID)
193 196 op_global_id, exists = GlobalId.from_xml_element(thread_id)
194 197 if exists:
195 198 opening_post = Post.objects.get(global_id=op_global_id)
196 199 else:
197 200 raise SyncException(EXCEPTION_OP)
198 201 else:
199 202 opening_post = None
200 203 tag_tags = tag_content.find(TAG_TAGS)
201 204 for tag_tag in tag_tags:
202 205 tag, created = Tag.objects.get_or_create(
203 206 name=tag_tag.text)
204 207 tags.append(tag)
205 208
206 209 # TODO Check that the replied posts are already present
207 210 # before adding new ones
208 211
209 212 files = []
210 213 tag_attachments = tag_content.find(TAG_ATTACHMENTS) or list()
211 214 tag_refs = tag_model.find(TAG_ATTACHMENT_REFS)
212 215 for attachment in tag_attachments:
213 216 tag_ref = tag_refs.find("{}[@ref='{}']".format(
214 217 TAG_ATTACHMENT_REF, attachment.text))
215 218 url = tag_ref.get(ATTR_URL)
216 219 attached_file = download(hostname + url)
217 220 if attached_file is None:
218 221 raise SyncException(EXCEPTION_DOWNLOAD)
219 222
220 223 hash = get_file_hash(attached_file)
221 224 if hash != attachment.text:
222 225 raise SyncException(EXCEPTION_HASH)
223 226
224 227 files.append(attached_file)
225 228
226 229 Post.objects.import_post(
227 230 title=title, text=text, pub_time=pub_time,
228 231 opening_post=opening_post, tags=tags,
229 232 global_id=global_id, files=files, tripcode=tripcode)
230 233 else:
231 234 raise SyncException(EXCEPTION_NODE.format(tag_status.text))
232 235
233 236 @staticmethod
234 237 def generate_response_list():
235 238 response = et.Element(TAG_RESPONSE)
236 239
237 240 status = et.SubElement(response, TAG_STATUS)
238 241 status.text = STATUS_SUCCESS
239 242
240 243 models = et.SubElement(response, TAG_MODELS)
241 244
242 245 for post in Post.objects.prefetch_related('global_id').all():
243 246 tag_id = et.SubElement(models, TAG_ID)
244 247 post.global_id.to_xml_element(tag_id)
245 248
246 249 return et.tostring(response, ENCODING_UNICODE)
247 250
248 251 @staticmethod
249 252 def _verify_model(content_str, tag_model):
250 253 """
251 254 Verifies all signatures for a single model.
252 255 """
253 256
254 257 signatures = []
255 258
256 259 tag_signatures = tag_model.find(TAG_SIGNATURES)
257 260 for tag_signature in tag_signatures:
258 261 signature_type = tag_signature.get(ATTR_TYPE)
259 262 signature_value = tag_signature.get(ATTR_VALUE)
260 263 signature_key = tag_signature.get(ATTR_KEY)
261 264
262 265 signature = Signature(key_type=signature_type,
263 266 key=signature_key,
264 267 signature=signature_value)
265 268
266 269 if not KeyPair.objects.verify(signature, content_str):
267 270 raise SyncException(EXCEPTION_SIGNATURE.format(content_str))
268 271
269 272 signatures.append(signature)
270 273
271 274 return signatures
272 275
273 276 @staticmethod
274 277 def _attachment_to_xml(tag_attachments, tag_refs, file, hash, url):
275 278 if tag_attachments is not None:
276 279 mimetype = get_file_mimetype(file)
277 280 attachment = et.SubElement(tag_attachments, TAG_ATTACHMENT)
278 281 attachment.set(ATTR_MIMETYPE, mimetype)
279 282 attachment.set(ATTR_ID_TYPE, ID_TYPE_MD5)
280 283 attachment.text = hash
281 284
282 285 attachment_ref = et.SubElement(tag_refs, TAG_ATTACHMENT_REF)
283 286 attachment_ref.set(ATTR_REF, hash)
284 287 attachment_ref.set(ATTR_URL, url)
General Comments 0
You need to be logged in to leave comments. Login now