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