##// END OF EJS Templates
Merge remote changes
bodqhrohro -
r1578:b47850ce merge 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,20 +1,22 b''
1 from django.core.management import BaseCommand
1 from django.core.management import BaseCommand
2 from django.db import transaction
2 from django.db import transaction
3
3
4 from boards.models import GlobalId
4 from boards.models import GlobalId
5
5
6 __author__ = 'neko259'
6 __author__ = 'neko259'
7
7
8
8
9 class Command(BaseCommand):
9 class Command(BaseCommand):
10 help = 'Removes local global ID cache'
10 help = 'Removes local global ID cache'
11
11
12 @transaction.atomic
12 @transaction.atomic
13 def handle(self, *args, **options):
13 def handle(self, *args, **options):
14 count = 0
14 count = 0
15 for global_id in GlobalId.objects.all():
15 for global_id in GlobalId.objects.exclude(content__isnull=True).exclude(
16 content=''):
16 if global_id.is_local() and global_id.content is not None:
17 if global_id.is_local() and global_id.content is not None:
17 global_id.content = None
18 global_id.content = None
18 global_id.save()
19 global_id.save()
20 global_id.signature_set.all().delete()
19 count += 1
21 count += 1
20 print('Invalidated {} caches.'.format(count))
22 print('Invalidated {} caches.'.format(count))
@@ -1,85 +1,90 b''
1 import re
1 import re
2 import xml.etree.ElementTree as ET
2 import xml.etree.ElementTree as ET
3
3
4 import httplib2
4 import httplib2
5 from django.core.management import BaseCommand
5 from django.core.management import BaseCommand
6
6
7 from boards.models import GlobalId
7 from boards.models import GlobalId
8 from boards.models.post.sync import SyncManager
8 from boards.models.post.sync import SyncManager, TAG_ID, TAG_VERSION
9
9
10 __author__ = 'neko259'
10 __author__ = 'neko259'
11
11
12
12
13 REGEX_GLOBAL_ID = re.compile(r'(\w+)::([\w\+/]+)::(\d+)')
13 REGEX_GLOBAL_ID = re.compile(r'(\w+)::([\w\+/]+)::(\d+)')
14
14
15
15
16 class Command(BaseCommand):
16 class Command(BaseCommand):
17 help = 'Send a sync or get request to the server.'
17 help = 'Send a sync or get request to the server.'
18
18
19 def add_arguments(self, parser):
19 def add_arguments(self, parser):
20 parser.add_argument('url', type=str, help='Server root url')
20 parser.add_argument('url', type=str, help='Server root url')
21 parser.add_argument('--global-id', type=str, default='',
21 parser.add_argument('--global-id', type=str, default='',
22 help='Post global ID')
22 help='Post global ID')
23 parser.add_argument('--split-query', type=int,
23 parser.add_argument('--split-query', type=int,
24 help='Split GET query into separate by the given'
24 help='Split GET query into separate by the given'
25 ' number of posts in one')
25 ' number of posts in one')
26
26
27 def handle(self, *args, **options):
27 def handle(self, *args, **options):
28 url = options.get('url')
28 url = options.get('url')
29
29
30 pull_url = url + 'api/sync/pull/'
30 list_url = url + 'api/sync/list/'
31 get_url = url + 'api/sync/get/'
31 get_url = url + 'api/sync/get/'
32 file_url = url[:-1]
32 file_url = url[:-1]
33
33
34 global_id_str = options.get('global_id')
34 global_id_str = options.get('global_id')
35 if global_id_str:
35 if global_id_str:
36 match = REGEX_GLOBAL_ID.match(global_id_str)
36 match = REGEX_GLOBAL_ID.match(global_id_str)
37 if match:
37 if match:
38 key_type = match.group(1)
38 key_type = match.group(1)
39 key = match.group(2)
39 key = match.group(2)
40 local_id = match.group(3)
40 local_id = match.group(3)
41
41
42 global_id = GlobalId(key_type=key_type, key=key,
42 global_id = GlobalId(key_type=key_type, key=key,
43 local_id=local_id)
43 local_id=local_id)
44
44
45 xml = GlobalId.objects.generate_request_get([global_id])
45 xml = GlobalId.objects.generate_request_get([global_id])
46 # body = urllib.parse.urlencode(data)
46 # body = urllib.parse.urlencode(data)
47 h = httplib2.Http()
47 h = httplib2.Http()
48 response, content = h.request(get_url, method="POST", body=xml)
48 response, content = h.request(get_url, method="POST", body=xml)
49
49
50 SyncManager.parse_response_get(content, file_url)
50 SyncManager.parse_response_get(content, file_url)
51 else:
51 else:
52 raise Exception('Invalid global ID')
52 raise Exception('Invalid global ID')
53 else:
53 else:
54 h = httplib2.Http()
54 h = httplib2.Http()
55 xml = GlobalId.objects.generate_request_pull()
55 xml = GlobalId.objects.generate_request_list()
56 response, content = h.request(pull_url, method="POST", body=xml)
56 response, content = h.request(list_url, method="POST", body=xml)
57
57
58 print(content.decode() + '\n')
58 print(content.decode() + '\n')
59
59
60 root = ET.fromstring(content)
60 root = ET.fromstring(content)
61 status = root.findall('status')[0].text
61 status = root.findall('status')[0].text
62 if status == 'success':
62 if status == 'success':
63 ids_to_sync = list()
63 ids_to_sync = list()
64
64
65 models = root.findall('models')[0]
65 models = root.findall('models')[0]
66 for model in models:
66 for model in models:
67 global_id, exists = GlobalId.from_xml_element(model)
67 tag_id = model.find(TAG_ID)
68 if not exists:
68 global_id, exists = GlobalId.from_xml_element(tag_id)
69 print(global_id)
69 tag_version = model.find(TAG_VERSION)
70 if tag_version is not None:
71 version = int(tag_version.text) or 1
72 else:
73 version = 1
74 if not exists or global_id.post.version < version:
70 ids_to_sync.append(global_id)
75 ids_to_sync.append(global_id)
71 print()
76 print('Starting sync...')
72
77
73 if len(ids_to_sync) > 0:
78 if len(ids_to_sync) > 0:
74 limit = options.get('split_query', len(ids_to_sync))
79 limit = options.get('split_query', len(ids_to_sync))
75 for offset in range(0, len(ids_to_sync), limit):
80 for offset in range(0, len(ids_to_sync), limit):
76 xml = GlobalId.objects.generate_request_get(ids_to_sync[offset:offset+limit])
81 xml = GlobalId.objects.generate_request_get(ids_to_sync[offset:offset+limit])
77 # body = urllib.parse.urlencode(data)
82 # body = urllib.parse.urlencode(data)
78 h = httplib2.Http()
83 h = httplib2.Http()
79 response, content = h.request(get_url, method="POST", body=xml)
84 response, content = h.request(get_url, method="POST", body=xml)
80
85
81 SyncManager.parse_response_get(content, file_url)
86 SyncManager.parse_response_get(content, file_url)
82 else:
87 else:
83 print('Nothing to get, everything synced')
88 print('Nothing to get, everything synced')
84 else:
89 else:
85 raise Exception('Invalid response status')
90 raise Exception('Invalid response status')
@@ -1,381 +1,396 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 try:
341 replacements[post_id] = absolute_post_id
342 absolute_post_id = str(Post.objects.get(id=post_id).global_id)
343 replacements[post_id] = absolute_post_id
344 except Post.DoesNotExist:
345 pass
342
346
343 text = self.get_raw_text() or ''
347 text = self.get_raw_text() or ''
344 for key in replacements:
348 for key in replacements:
345 text = text.replace('[post]{}[/post]'.format(key),
349 text = text.replace('[post]{}[/post]'.format(key),
346 '[post]{}[/post]'.format(replacements[key]))
350 '[post]{}[/post]'.format(replacements[key]))
347 text = text.replace('\r\n', '\n').replace('\r', '\n')
351 text = text.replace('\r\n', '\n').replace('\r', '\n')
348
352
349 return text
353 return text
350
354
351 def connect_threads(self, opening_posts):
355 def connect_threads(self, opening_posts):
352 for opening_post in opening_posts:
356 for opening_post in opening_posts:
353 threads = opening_post.get_threads().all()
357 threads = opening_post.get_threads().all()
354 for thread in threads:
358 for thread in threads:
355 if thread.can_bump():
359 if thread.can_bump():
356 thread.update_bump_status()
360 thread.update_bump_status()
357
361
358 thread.last_edit_time = self.last_edit_time
362 thread.last_edit_time = self.last_edit_time
359 thread.save(update_fields=['last_edit_time', 'status'])
363 thread.save(update_fields=['last_edit_time', 'status'])
360 self.threads.add(opening_post.get_thread())
364 self.threads.add(opening_post.get_thread())
361
365
362 def get_tripcode(self):
366 def get_tripcode(self):
363 if self.tripcode:
367 if self.tripcode:
364 return Tripcode(self.tripcode)
368 return Tripcode(self.tripcode)
365
369
366 def get_link_view(self):
370 def get_link_view(self):
367 """
371 """
368 Gets view of a reflink to the post.
372 Gets view of a reflink to the post.
369 """
373 """
370 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
374 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
371 self.id)
375 self.id)
372 if self.is_opening():
376 if self.is_opening():
373 result = '<b>{}</b>'.format(result)
377 result = '<b>{}</b>'.format(result)
374
378
375 return result
379 return result
376
380
377 def is_hidden(self) -> bool:
381 def is_hidden(self) -> bool:
378 return self.hidden
382 return self.hidden
379
383
380 def set_hidden(self, hidden):
384 def set_hidden(self, hidden):
381 self.hidden = hidden
385 self.hidden = hidden
386
387 def increment_version(self):
388 self.version = F('version') + 1
389
390 def clear_cache(self):
391 global_id = self.global_id
392 if global_id is not None and global_id.is_local()\
393 and global_id.content is not None:
394 global_id.content = None
395 global_id.save()
396 global_id.signature_set.all().delete()
@@ -1,284 +1,291 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)
233 print('Parsed post {}'.format(global_id))
230 else:
234 else:
231 raise SyncException(EXCEPTION_NODE.format(tag_status.text))
235 raise SyncException(EXCEPTION_NODE.format(tag_status.text))
232
236
233 @staticmethod
237 @staticmethod
234 def generate_response_pull():
238 def generate_response_list():
235 response = et.Element(TAG_RESPONSE)
239 response = et.Element(TAG_RESPONSE)
236
240
237 status = et.SubElement(response, TAG_STATUS)
241 status = et.SubElement(response, TAG_STATUS)
238 status.text = STATUS_SUCCESS
242 status.text = STATUS_SUCCESS
239
243
240 models = et.SubElement(response, TAG_MODELS)
244 models = et.SubElement(response, TAG_MODELS)
241
245
242 for post in Post.objects.prefetch_related('global_id').all():
246 for post in Post.objects.prefetch_related('global_id').all():
243 tag_id = et.SubElement(models, TAG_ID)
247 tag_model = et.SubElement(models, TAG_MODEL)
248 tag_id = et.SubElement(tag_model, TAG_ID)
244 post.global_id.to_xml_element(tag_id)
249 post.global_id.to_xml_element(tag_id)
250 tag_version = et.SubElement(tag_model, TAG_VERSION)
251 tag_version.text = str(post.version)
245
252
246 return et.tostring(response, ENCODING_UNICODE)
253 return et.tostring(response, ENCODING_UNICODE)
247
254
248 @staticmethod
255 @staticmethod
249 def _verify_model(content_str, tag_model):
256 def _verify_model(content_str, tag_model):
250 """
257 """
251 Verifies all signatures for a single model.
258 Verifies all signatures for a single model.
252 """
259 """
253
260
254 signatures = []
261 signatures = []
255
262
256 tag_signatures = tag_model.find(TAG_SIGNATURES)
263 tag_signatures = tag_model.find(TAG_SIGNATURES)
257 for tag_signature in tag_signatures:
264 for tag_signature in tag_signatures:
258 signature_type = tag_signature.get(ATTR_TYPE)
265 signature_type = tag_signature.get(ATTR_TYPE)
259 signature_value = tag_signature.get(ATTR_VALUE)
266 signature_value = tag_signature.get(ATTR_VALUE)
260 signature_key = tag_signature.get(ATTR_KEY)
267 signature_key = tag_signature.get(ATTR_KEY)
261
268
262 signature = Signature(key_type=signature_type,
269 signature = Signature(key_type=signature_type,
263 key=signature_key,
270 key=signature_key,
264 signature=signature_value)
271 signature=signature_value)
265
272
266 if not KeyPair.objects.verify(signature, content_str):
273 if not KeyPair.objects.verify(signature, content_str):
267 raise SyncException(EXCEPTION_SIGNATURE.format(content_str))
274 raise SyncException(EXCEPTION_SIGNATURE.format(content_str))
268
275
269 signatures.append(signature)
276 signatures.append(signature)
270
277
271 return signatures
278 return signatures
272
279
273 @staticmethod
280 @staticmethod
274 def _attachment_to_xml(tag_attachments, tag_refs, file, hash, url):
281 def _attachment_to_xml(tag_attachments, tag_refs, file, hash, url):
275 if tag_attachments is not None:
282 if tag_attachments is not None:
276 mimetype = get_file_mimetype(file)
283 mimetype = get_file_mimetype(file)
277 attachment = et.SubElement(tag_attachments, TAG_ATTACHMENT)
284 attachment = et.SubElement(tag_attachments, TAG_ATTACHMENT)
278 attachment.set(ATTR_MIMETYPE, mimetype)
285 attachment.set(ATTR_MIMETYPE, mimetype)
279 attachment.set(ATTR_ID_TYPE, ID_TYPE_MD5)
286 attachment.set(ATTR_ID_TYPE, ID_TYPE_MD5)
280 attachment.text = hash
287 attachment.text = hash
281
288
282 attachment_ref = et.SubElement(tag_refs, TAG_ATTACHMENT_REF)
289 attachment_ref = et.SubElement(tag_refs, TAG_ATTACHMENT_REF)
283 attachment_ref.set(ATTR_REF, hash)
290 attachment_ref.set(ATTR_REF, hash)
284 attachment_ref.set(ATTR_URL, url)
291 attachment_ref.set(ATTR_URL, url)
@@ -1,150 +1,150 b''
1 import xml.etree.ElementTree as et
1 import xml.etree.ElementTree as et
2 from django.db import models
2 from django.db import models
3 from boards.models import KeyPair
3 from boards.models import KeyPair
4
4
5
5
6 TAG_MODEL = 'model'
6 TAG_MODEL = 'model'
7 TAG_REQUEST = 'request'
7 TAG_REQUEST = 'request'
8 TAG_ID = 'id'
8 TAG_ID = 'id'
9
9
10 TYPE_GET = 'get'
10 TYPE_GET = 'get'
11 TYPE_PULL = 'pull'
11 TYPE_LIST = 'list'
12
12
13 ATTR_VERSION = 'version'
13 ATTR_VERSION = 'version'
14 ATTR_TYPE = 'type'
14 ATTR_TYPE = 'type'
15 ATTR_NAME = 'name'
15 ATTR_NAME = 'name'
16
16
17 ATTR_KEY = 'key'
17 ATTR_KEY = 'key'
18 ATTR_KEY_TYPE = 'type'
18 ATTR_KEY_TYPE = 'type'
19 ATTR_LOCAL_ID = 'local-id'
19 ATTR_LOCAL_ID = 'local-id'
20
20
21
21
22 class GlobalIdManager(models.Manager):
22 class GlobalIdManager(models.Manager):
23 def generate_request_get(self, global_id_list: list):
23 def generate_request_get(self, global_id_list: list):
24 """
24 """
25 Form a get request from a list of ModelId objects.
25 Form a get request from a list of ModelId objects.
26 """
26 """
27
27
28 request = et.Element(TAG_REQUEST)
28 request = et.Element(TAG_REQUEST)
29 request.set(ATTR_TYPE, TYPE_GET)
29 request.set(ATTR_TYPE, TYPE_GET)
30 request.set(ATTR_VERSION, '1.0')
30 request.set(ATTR_VERSION, '1.0')
31
31
32 model = et.SubElement(request, TAG_MODEL)
32 model = et.SubElement(request, TAG_MODEL)
33 model.set(ATTR_VERSION, '1.0')
33 model.set(ATTR_VERSION, '1.0')
34 model.set(ATTR_NAME, 'post')
34 model.set(ATTR_NAME, 'post')
35
35
36 for global_id in global_id_list:
36 for global_id in global_id_list:
37 tag_id = et.SubElement(model, TAG_ID)
37 tag_id = et.SubElement(model, TAG_ID)
38 global_id.to_xml_element(tag_id)
38 global_id.to_xml_element(tag_id)
39
39
40 return et.tostring(request, 'unicode')
40 return et.tostring(request, 'unicode')
41
41
42 def generate_request_pull(self):
42 def generate_request_list(self):
43 """
43 """
44 Form a pull request from a list of ModelId objects.
44 Form a pull request from a list of ModelId objects.
45 """
45 """
46
46
47 request = et.Element(TAG_REQUEST)
47 request = et.Element(TAG_REQUEST)
48 request.set(ATTR_TYPE, TYPE_PULL)
48 request.set(ATTR_TYPE, TYPE_LIST)
49 request.set(ATTR_VERSION, '1.0')
49 request.set(ATTR_VERSION, '1.0')
50
50
51 model = et.SubElement(request, TAG_MODEL)
51 model = et.SubElement(request, TAG_MODEL)
52 model.set(ATTR_VERSION, '1.0')
52 model.set(ATTR_VERSION, '1.0')
53 model.set(ATTR_NAME, 'post')
53 model.set(ATTR_NAME, 'post')
54
54
55 return et.tostring(request, 'unicode')
55 return et.tostring(request, 'unicode')
56
56
57 def global_id_exists(self, global_id):
57 def global_id_exists(self, global_id):
58 """
58 """
59 Checks if the same global id already exists in the system.
59 Checks if the same global id already exists in the system.
60 """
60 """
61
61
62 return self.filter(key=global_id.key,
62 return self.filter(key=global_id.key,
63 key_type=global_id.key_type,
63 key_type=global_id.key_type,
64 local_id=global_id.local_id).exists()
64 local_id=global_id.local_id).exists()
65
65
66
66
67 class GlobalId(models.Model):
67 class GlobalId(models.Model):
68 """
68 """
69 Global model ID and cache.
69 Global model ID and cache.
70 Key, key type and local ID make a single global identificator of the model.
70 Key, key type and local ID make a single global identificator of the model.
71 Content is an XML cache of the model that can be passed along between nodes
71 Content is an XML cache of the model that can be passed along between nodes
72 without manual serialization each time.
72 without manual serialization each time.
73 """
73 """
74 class Meta:
74 class Meta:
75 app_label = 'boards'
75 app_label = 'boards'
76
76
77 objects = GlobalIdManager()
77 objects = GlobalIdManager()
78
78
79 def __init__(self, *args, **kwargs):
79 def __init__(self, *args, **kwargs):
80 models.Model.__init__(self, *args, **kwargs)
80 models.Model.__init__(self, *args, **kwargs)
81
81
82 if 'key' in kwargs and 'key_type' in kwargs and 'local_id' in kwargs:
82 if 'key' in kwargs and 'key_type' in kwargs and 'local_id' in kwargs:
83 self.key = kwargs['key']
83 self.key = kwargs['key']
84 self.key_type = kwargs['key_type']
84 self.key_type = kwargs['key_type']
85 self.local_id = kwargs['local_id']
85 self.local_id = kwargs['local_id']
86
86
87 key = models.TextField()
87 key = models.TextField()
88 key_type = models.TextField()
88 key_type = models.TextField()
89 local_id = models.IntegerField()
89 local_id = models.IntegerField()
90 content = models.TextField(blank=True, null=True)
90 content = models.TextField(blank=True, null=True)
91
91
92 def __str__(self):
92 def __str__(self):
93 return '%s::%s::%d' % (self.key_type, self.key, self.local_id)
93 return '%s::%s::%d' % (self.key_type, self.key, self.local_id)
94
94
95 def to_xml_element(self, element: et.Element):
95 def to_xml_element(self, element: et.Element):
96 """
96 """
97 Exports global id to an XML element.
97 Exports global id to an XML element.
98 """
98 """
99
99
100 element.set(ATTR_KEY, self.key)
100 element.set(ATTR_KEY, self.key)
101 element.set(ATTR_KEY_TYPE, self.key_type)
101 element.set(ATTR_KEY_TYPE, self.key_type)
102 element.set(ATTR_LOCAL_ID, str(self.local_id))
102 element.set(ATTR_LOCAL_ID, str(self.local_id))
103
103
104 @staticmethod
104 @staticmethod
105 def from_xml_element(element: et.Element):
105 def from_xml_element(element: et.Element):
106 """
106 """
107 Parses XML id tag and gets global id from it.
107 Parses XML id tag and gets global id from it.
108
108
109 Arguments:
109 Arguments:
110 element -- the XML 'id' element
110 element -- the XML 'id' element
111
111
112 Returns:
112 Returns:
113 global_id -- id itself
113 global_id -- id itself
114 exists -- True if the global id was taken from database, False if it
114 exists -- True if the global id was taken from database, False if it
115 did not exist and was created.
115 did not exist and was created.
116 """
116 """
117
117
118 try:
118 try:
119 return GlobalId.objects.get(key=element.get(ATTR_KEY),
119 return GlobalId.objects.get(key=element.get(ATTR_KEY),
120 key_type=element.get(ATTR_KEY_TYPE),
120 key_type=element.get(ATTR_KEY_TYPE),
121 local_id=int(element.get(
121 local_id=int(element.get(
122 ATTR_LOCAL_ID))), True
122 ATTR_LOCAL_ID))), True
123 except GlobalId.DoesNotExist:
123 except GlobalId.DoesNotExist:
124 return GlobalId(key=element.get(ATTR_KEY),
124 return GlobalId(key=element.get(ATTR_KEY),
125 key_type=element.get(ATTR_KEY_TYPE),
125 key_type=element.get(ATTR_KEY_TYPE),
126 local_id=int(element.get(ATTR_LOCAL_ID))), False
126 local_id=int(element.get(ATTR_LOCAL_ID))), False
127
127
128 def is_local(self):
128 def is_local(self):
129 """Checks fo the ID is local model's"""
129 """Checks fo the ID is local model's"""
130 return KeyPair.objects.filter(
130 return KeyPair.objects.filter(
131 key_type=self.key_type, public_key=self.key).exists()
131 key_type=self.key_type, public_key=self.key).exists()
132
132
133
133
134 class Signature(models.Model):
134 class Signature(models.Model):
135 class Meta:
135 class Meta:
136 app_label = 'boards'
136 app_label = 'boards'
137
137
138 def __init__(self, *args, **kwargs):
138 def __init__(self, *args, **kwargs):
139 models.Model.__init__(self, *args, **kwargs)
139 models.Model.__init__(self, *args, **kwargs)
140
140
141 if 'key' in kwargs and 'key_type' in kwargs and 'signature' in kwargs:
141 if 'key' in kwargs and 'key_type' in kwargs and 'signature' in kwargs:
142 self.key_type = kwargs['key_type']
142 self.key_type = kwargs['key_type']
143 self.key = kwargs['key']
143 self.key = kwargs['key']
144 self.signature = kwargs['signature']
144 self.signature = kwargs['signature']
145
145
146 key_type = models.TextField()
146 key_type = models.TextField()
147 key = models.TextField()
147 key = models.TextField()
148 signature = models.TextField()
148 signature = models.TextField()
149
149
150 global_id = models.ForeignKey('GlobalId')
150 global_id = models.ForeignKey('GlobalId')
@@ -1,143 +1,159 b''
1 /*
1 /*
2 @licstart The following is the entire license notice for the
2 @licstart The following is the entire license notice for the
3 JavaScript code in this page.
3 JavaScript code in this page.
4
4
5
5
6 Copyright (C) 2013 neko259
6 Copyright (C) 2013 neko259
7
7
8 The JavaScript code in this page is free software: you can
8 The JavaScript code in this page is free software: you can
9 redistribute it and/or modify it under the terms of the GNU
9 redistribute it and/or modify it under the terms of the GNU
10 General Public License (GNU GPL) as published by the Free Software
10 General Public License (GNU GPL) as published by the Free Software
11 Foundation, either version 3 of the License, or (at your option)
11 Foundation, either version 3 of the License, or (at your option)
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 without even the implied warranty of MERCHANTABILITY or FITNESS
13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15
15
16 As additional permission under GNU GPL version 3 section 7, you
16 As additional permission under GNU GPL version 3 section 7, you
17 may distribute non-source (e.g., minimized or compacted) forms of
17 may distribute non-source (e.g., minimized or compacted) forms of
18 that code without the copy of the GNU GPL normally required by
18 that code without the copy of the GNU GPL normally required by
19 section 4, provided you include this license notice and a URL
19 section 4, provided you include this license notice and a URL
20 through which recipients can access the Corresponding Source.
20 through which recipients can access the Corresponding Source.
21
21
22 @licend The above is the entire license notice
22 @licend The above is the entire license notice
23 for the JavaScript code in this page.
23 for the JavaScript code in this page.
24 */
24 */
25
25
26 var CLOSE_BUTTON = '#form-close-button';
26 var CLOSE_BUTTON = '#form-close-button';
27 var REPLY_TO_MSG = '.reply-to-message';
27 var REPLY_TO_MSG = '.reply-to-message';
28 var REPLY_TO_MSG_ID = '#reply-to-message-id';
28 var REPLY_TO_MSG_ID = '#reply-to-message-id';
29
29
30 var $html = $("html, body");
30 var $html = $("html, body");
31
31
32 function moveCaretToEnd(el) {
32 function moveCaretToEnd(el) {
33 if (typeof el.selectionStart == "number") {
33 if (typeof el.selectionStart == "number") {
34 el.selectionStart = el.selectionEnd = el.value.length;
34 el.selectionStart = el.selectionEnd = el.value.length;
35 } else if (typeof el.createTextRange != "undefined") {
35 } else if (typeof el.createTextRange != "undefined") {
36 el.focus();
36 el.focus();
37 var range = el.createTextRange();
37 var range = el.createTextRange();
38 range.collapse(false);
38 range.collapse(false);
39 range.select();
39 range.select();
40 }
40 }
41 }
41 }
42
42
43 function getForm() {
43 function getForm() {
44 return $('.post-form-w');
44 return $('.post-form-w');
45 }
45 }
46
46
47 function resetFormPosition() {
47 function resetFormPosition() {
48 var form = getForm();
48 var form = getForm();
49 form.insertAfter($('.thread'));
49 form.insertAfter($('.thread'));
50
50
51 $(CLOSE_BUTTON).hide();
51 $(CLOSE_BUTTON).hide();
52 $(REPLY_TO_MSG).hide();
52 $(REPLY_TO_MSG).hide();
53 }
53 }
54
54
55 function showFormAfter(blockToInsertAfter) {
55 function showFormAfter(blockToInsertAfter) {
56 var form = getForm();
56 var form = getForm();
57 form.insertAfter(blockToInsertAfter);
57 form.insertAfter(blockToInsertAfter);
58
58
59 $(CLOSE_BUTTON).show();
59 $(CLOSE_BUTTON).show();
60 form.show();
60 form.show();
61 $(REPLY_TO_MSG_ID).text(blockToInsertAfter.attr('id'));
61 $(REPLY_TO_MSG_ID).text(blockToInsertAfter.attr('id'));
62 $(REPLY_TO_MSG).show();
62 $(REPLY_TO_MSG).show();
63 }
63 }
64
64
65 function addQuickReply(postId) {
65 function addQuickReply(postId) {
66 // If we click "reply" on the same post, it means "cancel"
66 // If we click "reply" on the same post, it means "cancel"
67 if (getForm().prev().attr('id') == postId) {
67 if (getForm().prev().attr('id') == postId) {
68 resetFormPosition();
68 resetFormPosition();
69 } else {
69 } else {
70 var blockToInsert = null;
70 var blockToInsert = null;
71 var textAreaJq = $('textarea');
71 var textAreaJq = $('textarea');
72 var postLinkRaw = '[post]' + postId + '[/post]'
72 var postLinkRaw = '[post]' + postId + '[/post]'
73 var textToAdd = '';
73 var textToAdd = '';
74
74
75 if (postId != null) {
75 if (postId != null) {
76 var post = $('#' + postId);
76 var post = $('#' + postId);
77
77
78 // If this is not OP, add reflink to the post. If there already is
78 // If this is not OP, add reflink to the post. If there already is
79 // the same reflink, don't add it again.
79 // the same reflink, don't add it again.
80 if (!post.is(':first-child') && textAreaJq.val().indexOf(postLinkRaw) < 0) {
80 if (!post.is(':first-child') && textAreaJq.val().indexOf(postLinkRaw) < 0) {
81 textToAdd += postLinkRaw + '\n';
81 textToAdd += postLinkRaw + '\n';
82 }
82 }
83
83
84 textAreaJq.val(textAreaJq.val()+ textToAdd);
84 textAreaJq.val(textAreaJq.val()+ textToAdd);
85 blockToInsert = post;
85 blockToInsert = post;
86 } else {
86 } else {
87 blockToInsert = $('.thread');
87 blockToInsert = $('.thread');
88 }
88 }
89 showFormAfter(blockToInsert);
89 showFormAfter(blockToInsert);
90
90
91 textAreaJq.focus();
91 textAreaJq.focus();
92
92
93 var textarea = document.getElementsByTagName('textarea')[0];
93 var textarea = document.getElementsByTagName('textarea')[0];
94 moveCaretToEnd(textarea);
94 moveCaretToEnd(textarea);
95 }
95 }
96 }
96 }
97
97
98 function addQuickQuote() {
98 function addQuickQuote() {
99 var textAreaJq = $('textarea');
100
101 var quoteButton = $("#quote-button");
102 var postId = quoteButton.attr('data-post-id');
103 if (postId != null && getForm().prev().attr('id') != postId) {
104 addQuickReply(postId);
105 }
106
99 var textToAdd = '';
107 var textToAdd = '';
100 var textAreaJq = $('textarea');
101 var selection = window.getSelection().toString();
108 var selection = window.getSelection().toString();
102 if (selection.length == 0) {
109 if (selection.length == 0) {
103 selection = $("#quote-button").attr('data-text');
110 selection = quoteButton.attr('data-text');
104 }
111 }
105 if (selection.length > 0) {
112 if (selection.length > 0) {
106 textToAdd += '[quote]' + selection + '[/quote]\n';
113 textToAdd += '[quote]' + selection + '[/quote]\n';
107 }
114 }
108
115
109 textAreaJq.val(textAreaJq.val()+ textToAdd);
116 textAreaJq.val(textAreaJq.val() + textToAdd);
110
117
111 textAreaJq.focus();
118 textAreaJq.focus();
112
119
113 var textarea = document.getElementsByTagName('textarea')[0];
120 var textarea = document.getElementsByTagName('textarea')[0];
114 moveCaretToEnd(textarea);
121 moveCaretToEnd(textarea);
115 }
122 }
116
123
117 function scrollToBottom() {
124 function scrollToBottom() {
118 $html.animate({scrollTop: $html.height()}, "fast");
125 $html.animate({scrollTop: $html.height()}, "fast");
119 }
126 }
120
127
121 function showQuoteButton() {
128 function showQuoteButton() {
122 var selection = window.getSelection().getRangeAt(0).getBoundingClientRect();
129 var selection = window.getSelection().getRangeAt(0).getBoundingClientRect();
123 var quoteButton = $("#quote-button");
130 var quoteButton = $("#quote-button");
124 if (selection.width > 0) {
131 if (selection.width > 0) {
125 // quoteButton.offset({ top: selection.top - selection.height, left: selection.left });
132 // quoteButton.offset({ top: selection.top - selection.height, left: selection.left });
126 quoteButton.css({top: selection.top + $(window).scrollTop() - 30, left: selection.left});
133 quoteButton.css({top: selection.top + $(window).scrollTop() - 30, left: selection.left});
127 quoteButton.show();
134 quoteButton.show();
128
135
129 var text = window.getSelection().toString();
136 var text = window.getSelection().toString();
130 quoteButton.attr('data-text', text);
137 quoteButton.attr('data-text', text);
138
139 var rect = window.getSelection().getRangeAt(0).getBoundingClientRect();
140 var element = $(document.elementFromPoint(rect.x, rect.y));
141 var postParent = element.parents('.post');
142 if (postParent.length > 0) {
143 quoteButton.attr('data-post-id', postParent.attr('id'));
144 } else {
145 quoteButton.attr('data-post-id', null);
146 }
131 } else {
147 } else {
132 quoteButton.hide();
148 quoteButton.hide();
133 }
149 }
134 }
150 }
135
151
136 $(document).ready(function() {
152 $(document).ready(function() {
137 $('body').on('mouseup', function() {
153 $('body').on('mouseup', function() {
138 showQuoteButton();
154 showQuoteButton();
139 });
155 });
140 $("#quote-button").click(function() {
156 $("#quote-button").click(function() {
141 addQuickQuote();
157 addQuickQuote();
142 })
158 })
143 }); No newline at end of file
159 });
@@ -1,89 +1,91 b''
1 from base64 import b64encode
1 from base64 import b64encode
2 import logging
2 import logging
3
3
4 from django.test import TestCase
4 from django.test import TestCase
5 from boards.models import KeyPair, GlobalId, Post, Signature
5 from boards.models import KeyPair, GlobalId, Post, Signature
6 from boards.models.post.sync import SyncManager
6 from boards.models.post.sync import SyncManager
7
7
8 logger = logging.getLogger(__name__)
8 logger = logging.getLogger(__name__)
9
9
10
10
11 class KeyTest(TestCase):
11 class KeyTest(TestCase):
12 def test_create_key(self):
12 def test_create_key(self):
13 key = KeyPair.objects.generate_key('ecdsa')
13 key = KeyPair.objects.generate_key('ecdsa')
14
14
15 self.assertIsNotNone(key, 'The key was not created.')
15 self.assertIsNotNone(key, 'The key was not created.')
16
16
17 def test_validation(self):
17 def test_validation(self):
18 key = KeyPair.objects.generate_key(key_type='ecdsa')
18 key = KeyPair.objects.generate_key(key_type='ecdsa')
19 message = 'msg'
19 message = 'msg'
20 signature_value = key.sign(message)
20 signature_value = key.sign(message)
21
21
22 signature = Signature(key_type='ecdsa', key=key.public_key,
22 signature = Signature(key_type='ecdsa', key=key.public_key,
23 signature=signature_value)
23 signature=signature_value)
24 valid = KeyPair.objects.verify(signature, message)
24 valid = KeyPair.objects.verify(signature, message)
25
25
26 self.assertTrue(valid, 'Message verification failed.')
26 self.assertTrue(valid, 'Message verification failed.')
27
27
28 def test_primary_constraint(self):
28 def test_primary_constraint(self):
29 KeyPair.objects.generate_key(key_type='ecdsa', primary=True)
29 KeyPair.objects.generate_key(key_type='ecdsa', primary=True)
30
30
31 with self.assertRaises(Exception):
31 with self.assertRaises(Exception):
32 KeyPair.objects.generate_key(key_type='ecdsa', primary=True)
32 KeyPair.objects.generate_key(key_type='ecdsa', primary=True)
33
33
34 def test_model_id_save(self):
34 def test_model_id_save(self):
35 model_id = GlobalId(key_type='test', key='test key', local_id='1')
35 model_id = GlobalId(key_type='test', key='test key', local_id='1')
36 model_id.save()
36 model_id.save()
37
37
38 def test_request_get(self):
38 def test_request_get(self):
39 post = self._create_post_with_key()
39 post = self._create_post_with_key()
40
40
41 request = GlobalId.objects.generate_request_get([post.global_id])
41 request = GlobalId.objects.generate_request_get([post.global_id])
42 logger.debug(request)
42 logger.debug(request)
43
43
44 key = KeyPair.objects.get(primary=True)
44 key = KeyPair.objects.get(primary=True)
45 self.assertTrue('<request type="get" version="1.0">'
45 self.assertTrue('<request type="get" version="1.0">'
46 '<model name="post" version="1.0">'
46 '<model name="post" version="1.0">'
47 '<id key="%s" local-id="1" type="%s" />'
47 '<id key="%s" local-id="1" type="%s" />'
48 '</model>'
48 '</model>'
49 '</request>' % (
49 '</request>' % (
50 key.public_key,
50 key.public_key,
51 key.key_type,
51 key.key_type,
52 ) in request,
52 ) in request,
53 'Wrong XML generated for the GET request.')
53 'Wrong XML generated for the GET request.')
54
54
55 def test_response_get(self):
55 def test_response_get(self):
56 post = self._create_post_with_key()
56 post = self._create_post_with_key()
57 reply_post = Post.objects.create_post(title='test_title',
57 reply_post = Post.objects.create_post(title='test_title',
58 text='[post]%d[/post]' % post.id,
58 text='[post]%d[/post]' % post.id,
59 thread=post.get_thread())
59 thread=post.get_thread())
60
60
61 response = SyncManager.generate_response_get([reply_post])
61 response = SyncManager.generate_response_get([reply_post])
62 logger.debug(response)
62 logger.debug(response)
63
63
64 key = KeyPair.objects.get(primary=True)
64 key = KeyPair.objects.get(primary=True)
65 self.assertTrue('<status>success</status>'
65 self.assertTrue('<status>success</status>'
66 '<models>'
66 '<models>'
67 '<model name="post">'
67 '<model name="post">'
68 '<content>'
68 '<content>'
69 '<id key="%s" local-id="%d" type="%s" />'
69 '<id key="%s" local-id="%d" type="%s" />'
70 '<title>test_title</title>'
70 '<title>test_title</title>'
71 '<text>[post]%s[/post]</text>'
71 '<text>[post]%s[/post]</text>'
72 '<thread><id key="%s" local-id="%d" type="%s" /></thread>'
72 '<thread><id key="%s" local-id="%d" type="%s" /></thread>'
73 '<pub-time>%s</pub-time>'
73 '<pub-time>%s</pub-time>'
74 '<version>%s</version>'
74 '</content>' % (
75 '</content>' % (
75 key.public_key,
76 key.public_key,
76 reply_post.id,
77 reply_post.id,
77 key.key_type,
78 key.key_type,
78 str(post.global_id),
79 str(post.global_id),
79 key.public_key,
80 key.public_key,
80 post.id,
81 post.id,
81 key.key_type,
82 key.key_type,
82 str(reply_post.get_pub_time_str()),
83 str(reply_post.get_pub_time_str()),
84 post.version,
83 ) in response,
85 ) in response,
84 'Wrong XML generated for the GET response.')
86 'Wrong XML generated for the GET response.')
85
87
86 def _create_post_with_key(self):
88 def _create_post_with_key(self):
87 KeyPair.objects.generate_key(primary=True)
89 KeyPair.objects.generate_key(primary=True)
88
90
89 return Post.objects.create_post(title='test_title', text='test_text')
91 return Post.objects.create_post(title='test_title', text='test_text')
@@ -1,102 +1,106 b''
1 from boards.models import KeyPair, Post, Tag
1 from boards.models import KeyPair, Post, Tag
2 from boards.models.post.sync import SyncManager
2 from boards.models.post.sync import SyncManager
3 from boards.tests.mocks import MockRequest
3 from boards.tests.mocks import MockRequest
4 from boards.views.sync import response_get
4 from boards.views.sync import response_get
5
5
6 __author__ = 'neko259'
6 __author__ = 'neko259'
7
7
8
8
9 from django.test import TestCase
9 from django.test import TestCase
10
10
11
11
12 class SyncTest(TestCase):
12 class SyncTest(TestCase):
13 def test_get(self):
13 def test_get(self):
14 """
14 """
15 Forms a GET request of a post and checks the response.
15 Forms a GET request of a post and checks the response.
16 """
16 """
17
17
18 key = KeyPair.objects.generate_key(primary=True)
18 key = KeyPair.objects.generate_key(primary=True)
19 tag = Tag.objects.create(name='tag1')
19 tag = Tag.objects.create(name='tag1')
20 post = Post.objects.create_post(title='test_title',
20 post = Post.objects.create_post(title='test_title',
21 text='test_text\rline two',
21 text='test_text\rline two',
22 tags=[tag])
22 tags=[tag])
23
23
24 request = MockRequest()
24 request = MockRequest()
25 request.body = (
25 request.body = (
26 '<request type="get" version="1.0">'
26 '<request type="get" version="1.0">'
27 '<model name="post" version="1.0">'
27 '<model name="post" version="1.0">'
28 '<id key="%s" local-id="%d" type="%s" />'
28 '<id key="%s" local-id="%d" type="%s" />'
29 '</model>'
29 '</model>'
30 '</request>' % (post.global_id.key,
30 '</request>' % (post.global_id.key,
31 post.id,
31 post.id,
32 post.global_id.key_type)
32 post.global_id.key_type)
33 )
33 )
34
34
35 response = response_get(request).content.decode()
35 response = response_get(request).content.decode()
36 self.assertTrue(
36 self.assertTrue(
37 '<status>success</status>'
37 '<status>success</status>'
38 '<models>'
38 '<models>'
39 '<model name="post">'
39 '<model name="post">'
40 '<content>'
40 '<content>'
41 '<id key="%s" local-id="%d" type="%s" />'
41 '<id key="%s" local-id="%d" type="%s" />'
42 '<title>%s</title>'
42 '<title>%s</title>'
43 '<text>%s</text>'
43 '<text>%s</text>'
44 '<tags><tag>%s</tag></tags>'
44 '<tags><tag>%s</tag></tags>'
45 '<pub-time>%s</pub-time>'
45 '<pub-time>%s</pub-time>'
46 '<version>%s</version>'
46 '</content>' % (
47 '</content>' % (
47 post.global_id.key,
48 post.global_id.key,
48 post.global_id.local_id,
49 post.global_id.local_id,
49 post.global_id.key_type,
50 post.global_id.key_type,
50 post.title,
51 post.title,
51 post.get_sync_text(),
52 post.get_sync_text(),
52 post.get_thread().get_tags().first().name,
53 post.get_thread().get_tags().first().name,
53 post.get_pub_time_str(),
54 post.get_pub_time_str(),
55 post.version,
54 ) in response,
56 ) in response,
55 'Wrong response generated for the GET request.')
57 'Wrong response generated for the GET request.')
56
58
57 post.delete()
59 post.delete()
58 key.delete()
60 key.delete()
59
61
60 KeyPair.objects.generate_key(primary=True)
62 KeyPair.objects.generate_key(primary=True)
61
63
62 SyncManager.parse_response_get(response, None)
64 SyncManager.parse_response_get(response, None)
63 self.assertEqual(1, Post.objects.count(),
65 self.assertEqual(1, Post.objects.count(),
64 'Post was not created from XML response.')
66 'Post was not created from XML response.')
65
67
66 parsed_post = Post.objects.first()
68 parsed_post = Post.objects.first()
67 self.assertEqual('tag1',
69 self.assertEqual('tag1',
68 parsed_post.get_thread().get_tags().first().name,
70 parsed_post.get_thread().get_tags().first().name,
69 'Invalid tag was parsed.')
71 'Invalid tag was parsed.')
70
72
71 SyncManager.parse_response_get(response, None)
73 SyncManager.parse_response_get(response, None)
72 self.assertEqual(1, Post.objects.count(),
74 self.assertEqual(1, Post.objects.count(),
73 'The same post was imported twice.')
75 'The same post was imported twice.')
74
76
75 self.assertEqual(1, parsed_post.global_id.signature_set.count(),
77 self.assertEqual(1, parsed_post.global_id.signature_set.count(),
76 'Signature was not saved.')
78 'Signature was not saved.')
77
79
78 post = parsed_post
80 post = parsed_post
79
81
80 # Trying to sync the same once more
82 # Trying to sync the same once more
81 response = response_get(request).content.decode()
83 response = response_get(request).content.decode()
82
84
83 self.assertTrue(
85 self.assertTrue(
84 '<status>success</status>'
86 '<status>success</status>'
85 '<models>'
87 '<models>'
86 '<model name="post">'
88 '<model name="post">'
87 '<content>'
89 '<content>'
88 '<id key="%s" local-id="%d" type="%s" />'
90 '<id key="%s" local-id="%d" type="%s" />'
89 '<title>%s</title>'
91 '<title>%s</title>'
90 '<text>%s</text>'
92 '<text>%s</text>'
91 '<tags><tag>%s</tag></tags>'
93 '<tags><tag>%s</tag></tags>'
92 '<pub-time>%s</pub-time>'
94 '<pub-time>%s</pub-time>'
95 '<version>%s</version>'
93 '</content>' % (
96 '</content>' % (
94 post.global_id.key,
97 post.global_id.key,
95 post.global_id.local_id,
98 post.global_id.local_id,
96 post.global_id.key_type,
99 post.global_id.key_type,
97 post.title,
100 post.title,
98 post.get_sync_text(),
101 post.get_sync_text(),
99 post.get_thread().get_tags().first().name,
102 post.get_thread().get_tags().first().name,
100 post.get_pub_time_str(),
103 post.get_pub_time_str(),
104 post.version,
101 ) in response,
105 ) in response,
102 'Wrong response generated for the GET request.')
106 'Wrong response generated for the GET request.')
@@ -1,99 +1,96 b''
1 from django.conf.urls import url
1 from django.conf.urls import url
2 #from django.views.i18n import javascript_catalog
3
2
4 import neboard
3 import neboard
5
4
6 from boards import views
5 from boards import views
7 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
6 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
8 from boards.views import api, tag_threads, all_threads, \
7 from boards.views import api, tag_threads, all_threads, \
9 settings, all_tags, feed
8 settings, all_tags, feed
10 from boards.views.authors import AuthorsView
9 from boards.views.authors import AuthorsView
11 from boards.views.notifications import NotificationView
10 from boards.views.notifications import NotificationView
12 from boards.views.search import BoardSearchView
13 from boards.views.static import StaticPageView
11 from boards.views.static import StaticPageView
14 from boards.views.preview import PostPreviewView
12 from boards.views.preview import PostPreviewView
15 from boards.views.sync import get_post_sync_data, response_get, response_pull
13 from boards.views.sync import get_post_sync_data, response_get, response_list
16 from boards.views.random import RandomImageView
14 from boards.views.random import RandomImageView
17 from boards.views.tag_gallery import TagGalleryView
15 from boards.views.tag_gallery import TagGalleryView
18 from boards.views.translation import cached_javascript_catalog
16 from boards.views.translation import cached_javascript_catalog
19
17
20
18
21 js_info_dict = {
19 js_info_dict = {
22 'packages': ('boards',),
20 'packages': ('boards',),
23 }
21 }
24
22
25 urlpatterns = [
23 urlpatterns = [
26 # /boards/
24 # /boards/
27 url(r'^$', all_threads.AllThreadsView.as_view(), name='index'),
25 url(r'^$', all_threads.AllThreadsView.as_view(), name='index'),
28
26
29 # /boards/tag/tag_name/
27 # /boards/tag/tag_name/
30 url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(),
28 url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(),
31 name='tag'),
29 name='tag'),
32
30
33 # /boards/thread/
31 # /boards/thread/
34 url(r'^thread/(?P<post_id>\d+)/$', views.thread.NormalThreadView.as_view(),
32 url(r'^thread/(?P<post_id>\d+)/$', views.thread.NormalThreadView.as_view(),
35 name='thread'),
33 name='thread'),
36 url(r'^thread/(?P<post_id>\d+)/mode/gallery/$', views.thread.GalleryThreadView.as_view(),
34 url(r'^thread/(?P<post_id>\d+)/mode/gallery/$', views.thread.GalleryThreadView.as_view(),
37 name='thread_gallery'),
35 name='thread_gallery'),
38 url(r'^thread/(?P<post_id>\d+)/mode/tree/$', views.thread.TreeThreadView.as_view(),
36 url(r'^thread/(?P<post_id>\d+)/mode/tree/$', views.thread.TreeThreadView.as_view(),
39 name='thread_tree'),
37 name='thread_tree'),
40 # /feed/
38 # /feed/
41 url(r'^feed/$', views.feed.FeedView.as_view(), name='feed'),
39 url(r'^feed/$', views.feed.FeedView.as_view(), name='feed'),
42
40
43 url(r'^settings/$', settings.SettingsView.as_view(), name='settings'),
41 url(r'^settings/$', settings.SettingsView.as_view(), name='settings'),
44 url(r'^tags/(?P<query>\w+)?/?$', all_tags.AllTagsView.as_view(), name='tags'),
42 url(r'^tags/(?P<query>\w+)?/?$', all_tags.AllTagsView.as_view(), name='tags'),
45 url(r'^authors/$', AuthorsView.as_view(), name='authors'),
43 url(r'^authors/$', AuthorsView.as_view(), name='authors'),
46
44
47 url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'),
45 url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'),
48 url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(),
46 url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(),
49 name='staticpage'),
47 name='staticpage'),
50
48
51 url(r'^random/$', RandomImageView.as_view(), name='random'),
49 url(r'^random/$', RandomImageView.as_view(), name='random'),
52 url(r'^tag/(?P<tag_name>\w+)/gallery/$', TagGalleryView.as_view(), name='tag_gallery'),
50 url(r'^tag/(?P<tag_name>\w+)/gallery/$', TagGalleryView.as_view(), name='tag_gallery'),
53
51
54 # RSS feeds
52 # RSS feeds
55 url(r'^rss/$', AllThreadsFeed()),
53 url(r'^rss/$', AllThreadsFeed()),
56 url(r'^page/(?P<page>\d+)/rss/$', AllThreadsFeed()),
54 url(r'^page/(?P<page>\d+)/rss/$', AllThreadsFeed()),
57 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
55 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
58 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
56 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
59 url(r'^thread/(?P<post_id>\d+)/rss/$', ThreadPostsFeed()),
57 url(r'^thread/(?P<post_id>\d+)/rss/$', ThreadPostsFeed()),
60
58
61 # i18n
59 # i18n
62 url(r'^jsi18n/$', cached_javascript_catalog, js_info_dict,
60 url(r'^jsi18n/$', cached_javascript_catalog, js_info_dict,
63 name='js_info_dict'),
61 name='js_info_dict'),
64
62
65 # API
63 # API
66 url(r'^api/post/(?P<post_id>\d+)/$', api.get_post, name="get_post"),
64 url(r'^api/post/(?P<post_id>\d+)/$', api.get_post, name="get_post"),
67 url(r'^api/diff_thread/$', api.api_get_threaddiff, name="get_thread_diff"),
65 url(r'^api/diff_thread/$', api.api_get_threaddiff, name="get_thread_diff"),
68 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
66 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
69 name='get_threads'),
67 name='get_threads'),
70 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
68 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
71 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
69 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
72 name='get_thread'),
70 name='get_thread'),
73 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
71 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
74 name='add_post'),
72 name='add_post'),
75 url(r'^api/notifications/(?P<username>\w+)/$', api.api_get_notifications,
73 url(r'^api/notifications/(?P<username>\w+)/$', api.api_get_notifications,
76 name='api_notifications'),
74 name='api_notifications'),
77 url(r'^api/preview/$', api.api_get_preview, name='preview'),
75 url(r'^api/preview/$', api.api_get_preview, name='preview'),
78 url(r'^api/new_posts/$', api.api_get_new_posts, name='new_posts'),
76 url(r'^api/new_posts/$', api.api_get_new_posts, name='new_posts'),
79
77
80 # Sync protocol API
78 # Sync protocol API
81 url(r'^api/sync/pull/$', response_pull, name='api_sync_pull'),
79 url(r'^api/sync/list/$', response_list, name='api_sync_list'),
82 url(r'^api/sync/get/$', response_get, name='api_sync_pull'),
80 url(r'^api/sync/get/$', response_get, name='api_sync_get'),
83 # TODO 'get' request
84
81
85 # Notifications
82 # Notifications
86 url(r'^notifications/(?P<username>\w+)/$', NotificationView.as_view(), name='notifications'),
83 url(r'^notifications/(?P<username>\w+)/$', NotificationView.as_view(), name='notifications'),
87 url(r'^notifications/$', NotificationView.as_view(), name='notifications'),
84 url(r'^notifications/$', NotificationView.as_view(), name='notifications'),
88
85
89 # Post preview
86 # Post preview
90 url(r'^preview/$', PostPreviewView.as_view(), name='preview'),
87 url(r'^preview/$', PostPreviewView.as_view(), name='preview'),
91 url(r'^post_xml/(?P<post_id>\d+)$', get_post_sync_data,
88 url(r'^post_xml/(?P<post_id>\d+)$', get_post_sync_data,
92 name='post_sync_data'),
89 name='post_sync_data'),
93 ]
90 ]
94
91
95 # Search
92 # Search
96 if 'haystack' in neboard.settings.INSTALLED_APPS:
93 if 'haystack' in neboard.settings.INSTALLED_APPS:
97 from boards.views.search import BoardSearchView
94 from boards.views.search import BoardSearchView
98 urlpatterns.append(url(r'^search/$', BoardSearchView.as_view(), name='search'))
95 urlpatterns.append(url(r'^search/$', BoardSearchView.as_view(), name='search'))
99
96
@@ -1,62 +1,55 b''
1 import xml.etree.ElementTree as et
1 import xml.etree.ElementTree as et
2 import xml.dom.minidom
3
2
4 from django.http import HttpResponse, Http404
3 from django.http import HttpResponse, Http404
5 from boards.models import GlobalId, Post
4 from boards.models import GlobalId, Post
6 from boards.models.post.sync import SyncManager
5 from boards.models.post.sync import SyncManager
7
6
8
7
9 def response_pull(request):
8 def response_list(request):
10 request_xml = request.body
9 request_xml = request.body
11
10
12 if request_xml is None:
11 if request_xml is None or len(request_xml) == 0:
13 return HttpResponse(content='Use the API')
12 return HttpResponse(content='Use the API')
14
13
15 response_xml = SyncManager.generate_response_pull()
14 response_xml = SyncManager.generate_response_list()
16
15
17 return HttpResponse(content=response_xml)
16 return HttpResponse(content=response_xml)
18
17
19
18
20 def response_get(request):
19 def response_get(request):
21 """
20 """
22 Processes a GET request with post ID list and returns the posts XML list.
21 Processes a GET request with post ID list and returns the posts XML list.
23 Request should contain an 'xml' post attribute with the actual request XML.
22 Request should contain an 'xml' post attribute with the actual request XML.
24 """
23 """
25
24
26 request_xml = request.body
25 request_xml = request.body
27
26
28 if request_xml is None:
27 if request_xml is None or len(request_xml) == 0:
29 return HttpResponse(content='Use the API')
28 return HttpResponse(content='Use the API')
30
29
31 posts = []
30 posts = []
32
31
33 root_tag = et.fromstring(request_xml)
32 root_tag = et.fromstring(request_xml)
34 model_tag = root_tag[0]
33 model_tag = root_tag[0]
35 for id_tag in model_tag:
34 for id_tag in model_tag:
36 global_id, exists = GlobalId.from_xml_element(id_tag)
35 global_id, exists = GlobalId.from_xml_element(id_tag)
37 if exists:
36 if exists:
38 posts.append(Post.objects.get(global_id=global_id))
37 posts.append(Post.objects.get(global_id=global_id))
39
38
40 response_xml = SyncManager.generate_response_get(posts)
39 response_xml = SyncManager.generate_response_get(posts)
41
40
42 return HttpResponse(content=response_xml)
41 return HttpResponse(content=response_xml)
43
42
44
43
45 def get_post_sync_data(request, post_id):
44 def get_post_sync_data(request, post_id):
46 try:
45 try:
47 post = Post.objects.get(id=post_id)
46 post = Post.objects.get(id=post_id)
48 except Post.DoesNotExist:
47 except Post.DoesNotExist:
49 raise Http404()
48 raise Http404()
50
49
51 xml_str = SyncManager.generate_response_get([post])
50 xml_str = SyncManager.generate_response_get([post])
52
51
53 xml_repr = xml.dom.minidom.parseString(xml_str)
54 xml_repr = xml_repr.toprettyxml()
55
56 content = '=Global ID=\n%s\n\n=XML=\n%s' \
57 % (post.global_id, xml_repr)
58
59 return HttpResponse(
52 return HttpResponse(
60 content_type='text/plain; charset=utf-8',
53 content_type='text/xml; charset=utf-8',
61 content=content,
54 content=xml_str,
62 ) No newline at end of file
55 )
@@ -1,81 +1,81 b''
1 # INTRO #
1 # INTRO #
2
2
3 The API is provided to query the data from a neaboard server by any client
3 The API is provided to query the data from a neaboard server by any client
4 application.
4 application.
5
5
6 Tha data is returned in the json format and got by an http query.
6 Tha data is returned in the json format and got by an http query.
7
7
8 # METHODS #
8 # METHODS #
9
9
10 ## Threads ##
10 ## Threads ##
11
11
12 /api/threads/N/?offset=M&tag=O
12 /api/threads/N/?offset=M&tag=O
13
13
14 Get a thread list. You will get ``N`` threads (required parameter) starting from
14 Get a thread list. You will get ``N`` threads (required parameter) starting from
15 ``M``th one (optional parameter, default is 0) with the tag ``O`` (optional parameter,
15 ``M``th one (optional parameter, default is 0) with the tag ``O`` (optional parameter,
16 threads with any tags are shown by default).
16 threads with any tags are shown by default).
17
17
18 ## Tags ##
18 ## Tags ##
19
19
20 /api/tags/
20 /api/tags/
21
21
22 Get all active tag list. Active tag is a tag that has at least 1 active thread
22 Get all active tag list. Active tag is a tag that has at least 1 active thread
23 associated with it.
23 associated with it.
24
24
25 ## Thread ##
25 ## Thread ##
26
26
27 /api/thread/N/
27 /api/thread/N/
28
28
29 Get all ``N``th thread post. ``N`` is an opening post ID for the thread.
29 Get all ``N``th thread post. ``N`` is an opening post ID for the thread.
30
30
31 Output format:
31 Output format:
32
32
33 * ``posts``: list of posts
33 * ``posts``: list of posts
34 * ``last_update``: last update timestamp
34 * ``last_update``: last update timestamp
35
35
36 ## Thread diff ##
36 ## Thread diff ##
37
37
38 /api/diff_thread
38 /api/diff_thread
39
39
40 Parameters:
40 Parameters:
41
41
42 * ``thread``: thread id
42 * ``thread``: thread id
43 * ``last_update``: last update timestamp
43 * ``last_update``: last update timestamp
44 * ``last_post``: last added post id
44 * ``last_post``: last added post id
45
45
46 Get the diff of the thread in the `O``
46 Get the diff of the thread in the ``O``
47 format. 2 formats are available: ``html`` (used in AJAX thread update) and
47 format. 2 formats are available: ``html`` (used in AJAX thread update) and
48 ``json``. The default format is ``html``. Return list format:
48 ``json``. The default format is ``html``. Return list format:
49
49
50 * ``added``: list of added posts
50 * ``added``: list of added posts
51 * ``updated``: list of updated posts
51 * ``updated``: list of updated posts
52 * ``last_update``: last update timestamp
52 * ``last_update``: last update timestamp
53
53
54 ## Notifications ##
54 ## Notifications ##
55
55
56 /api/notifications/<username>/[?last=<id>]
56 /api/notifications/<username>/[?last=<id>]
57
57
58 Get user notifications for user starting from the post ID.
58 Get user notifications for user starting from the post ID.
59
59
60 * ``username``: name of the notified user
60 * ``username``: name of the notified user
61 * ``id``: ID of a last notification post
61 * ``id``: ID of a last notification post
62
62
63 ## General info ##
63 ## General info ##
64
64
65 In case of incorrect request you can get http error 404.
65 In case of incorrect request you can get http error 404.
66
66
67 Response JSON for a post or thread contains:
67 Response JSON for a post or thread contains:
68
68
69 * ``id``
69 * ``id``
70 * ``title``
70 * ``title``
71 * ``text``
71 * ``text``
72 * ``image`` (if image available)
72 * ``image`` (if image available)
73 * ``image_preview`` (if image available)
73 * ``image_preview`` (if image available)
74 * ``bump_time`` (for threads)
74 * ``bump_time`` (for threads)
75
75
76 In future, it will also contain:
76 In future, it will also contain:
77
77
78 * tags list (for thread)
78 * tags list (for thread)
79 * publishing time
79 * publishing time
80 * bump time
80 * bump time
81 * reply IDs (if available)
81 * reply IDs (if available)
@@ -1,203 +1,210 b''
1 # 0 Title #
1 # 0 Title #
2
2
3 DIP-1 Common protocol description
3 DIP-1 Common protocol description
4
4
5 # 1 Intro #
5 # 1 Intro #
6
6
7 This document describes the Data Interchange Protocol (DIP), designed to
7 This document describes the Data Interchange Protocol (DIP), designed to
8 exchange filtered data that can be stored as a graph structure between
8 exchange filtered data that can be stored as a graph structure between
9 network nodes.
9 network nodes.
10
10
11 # 2 Purpose #
11 # 2 Purpose #
12
12
13 This protocol will be used to share the models (originally imageboard posts)
13 This protocol will be used to share the models (originally imageboard posts)
14 across multiple servers. The main differnce of this protocol is that the node
14 across multiple servers. The main differnce of this protocol is that the node
15 can specify what models it wants to get and from whom. The nodes can get
15 can specify what models it wants to get and from whom. The nodes can get
16 models from a specific server, or from all except some specific servers. Also
16 models from a specific server, or from all except some specific servers. Also
17 the models can be filtered by timestamps or tags.
17 the models can be filtered by timestamps or tags.
18
18
19 # 3 Protocol description #
19 # 3 Protocol description #
20
20
21 The node requests other node's changes list since some time (since epoch if
21 The node requests other node's changes list since some time (since epoch if
22 this is the start). The other node sends a list of post ids or posts in the
22 this is the start). The other node sends a list of post ids or posts in the
23 XML format.
23 XML format.
24
24
25 Protocol version is the version of the sync api. Model version is the version
25 Protocol version is the version of the sync api. Model version is the version
26 of data models. If at least one of them is different, the sync cannot be
26 of data models. If at least one of them is different, the sync cannot be
27 performed.
27 performed.
28
28
29 The node signs the data with its keys. The receiving node saves the key at the
29 The node signs the data with its keys. The receiving node saves the key at the
30 first sync and checks it every time. If the key has changed, the info won't be
30 first sync and checks it every time. If the key has changed, the info won't be
31 saved from the node (or the node id must be changed). A model can be signed
31 saved from the node (or the node id must be changed). A model can be signed
32 with several keys but at least one of them must be the same as in the global
32 with several keys but at least one of them must be the same as in the global
33 ID to verify the sender.
33 ID to verify the sender.
34
34
35 Each node can have several keys. Nodes can have shared keys to serve as a pool
35 Each node can have several keys. Nodes can have shared keys to serve as a pool
36 (several nodes with the same key).
36 (several nodes with the same key).
37
37
38 Each post has an ID in the unique format: key-type::key::local-id
38 Each post has an ID in the unique format: key-type::key::local-id
39
39
40 All requests pass a request type, protocol and model versions, and a list of
40 All requests pass a request type, protocol and model versions, and a list of
41 optional arguments used for filtering.
41 optional arguments used for filtering.
42
42
43 Each request has its own version. Version consists of 2 numbers: first is
43 Each request has its own version. Version consists of 2 numbers: first is
44 incompatible version (1.3 and 2.0 are not compatible and must not be in sync)
44 incompatible version (1.3 and 2.0 are not compatible and must not be in sync)
45 and the second one is minor and compatible (for example, new optional field
45 and the second one is minor and compatible (for example, new optional field
46 is added which will be igroned by those who don't support it yet).
46 is added which will be igroned by those who don't support it yet).
47
47
48 Post edits and reflinks are not saved to the sync model. The replied post ID
48 Post edits and reflinks are not saved to the sync model. The replied post ID
49 can be got from the post text, and reflinks can be computed when loading
49 can be got from the post text, and reflinks can be computed when loading
50 posts. The edit time is not saved because a foreign post can be 'edited' (new
50 posts. The edit time is not saved because a foreign post can be 'edited' (new
51 replies are added) but the signature must not change (so we can't update the
51 replies are added) but the signature must not change (so we can't update the
52 content). The inner posts can be edited, and the signature will change then
52 content). The inner posts can be edited, and the signature will change then
53 but the local-id won't, so the other node can detect that and replace the post
53 but the local-id won't, so the other node can detect that and replace the post
54 instead of adding a new one.
54 instead of adding a new one.
55
55
56 ## 3.1 Requests ##
56 ## 3.1 Requests ##
57
57
58 There is no constraint on how the server should calculate the request. The
58 There is no constraint on how the server should calculate the request. The
59 server can return any information by any filter and the requesting node is
59 server can return any information by any filter and the requesting node is
60 responsible for validating it.
60 responsible for validating it.
61
61
62 The server is required to return the status of request. See 3.2 for details.
62 The server is required to return the status of request. See 3.2 for details.
63
63
64 ### 3.1.1 pull ###
64 ### 3.1.1 list ###
65
65
66 "pull" request gets the desired model id list by the given filter (e.g. thread, tags,
66 "list" request gets the desired model id list by the given filter (e.g. thread, tags,
67 author)
67 author)
68
68
69 Sample request is as follows:
69 Sample request is as follows:
70
70
71 <?xml version="1.1" encoding="UTF-8" ?>
71 <?xml version="1.1" encoding="UTF-8" ?>
72 <request version="1.0" type="pull">
72 <request version="1.0" type="list">
73 <model version="1.0" name="post">
73 <model version="1.0" name="post">
74 <timestamp_from>0</timestamp_from>
74 <timestamp_from>0</timestamp_from>
75 <timestamp_to>0</timestamp_to>
75 <timestamp_to>0</timestamp_to>
76 <tags>
76 <tags>
77 <tag>tag1</tag>
77 <tag>tag1</tag>
78 </tags>
78 </tags>
79 <sender>
79 <sender>
80 <allow>
80 <allow>
81 <key>abcehy3h9t</key>
81 <key>abcehy3h9t</key>
82 <key>ehoehyoe</key>
82 <key>ehoehyoe</key>
83 </allow>
83 </allow>
84 <!-- There can be only allow block (all other are denied) or deny block (all other are allowed) -->
84 <!-- There can be only allow block (all other are denied) or deny block (all other are allowed) -->
85 </sender>
85 </sender>
86 </model>
86 </model>
87 </request>
87 </request>
88
88
89 Under the <model> tag there are filters. Filters for the "post" model can
89 Under the <model> tag there are filters. Filters for the "post" model can
90 be found in DIP-2.
90 be found in DIP-2.
91
91
92 Sample response:
92 Sample response:
93
93
94 <?xml version="1.1" encoding="UTF-8" ?>
94 <?xml version="1.1" encoding="UTF-8" ?>
95 <response>
95 <response>
96 <status>success</status>
96 <status>success</status>
97 <models>
97 <models>
98 <id key="id1" type="ecdsa" local-id="1" />
98 <model>
99 <id key="id1" type="ecdsa" local-id="2" />
99 <id key="id1" type="ecdsa" local-id="1">
100 <id key="id2" type="ecdsa" local-id="1" />
100 <version>1</version>
101 <id key="id2" type="ecdsa" local-id="5" />
101 </model>
102 <model>
103 <id key="id1" type="ecdsa" local-id="2" />
104 </model>
105 <model>
106 <id key="id2" type="ecdsa" local-id="1" />
107 <some-valuable-attr>value</some-valuable-attr>
108 </model>
102 </models>
109 </models>
103 </response>
110 </response>
104
111
105 ### 3.1.2 get ###
112 ### 3.1.2 get ###
106
113
107 "get" gets models by id list.
114 "get" gets models by id list.
108
115
109 Sample request:
116 Sample request:
110
117
111 <?xml version="1.1" encoding="UTF-8" ?>
118 <?xml version="1.1" encoding="UTF-8" ?>
112 <request version="1.0" type="get">
119 <request version="1.0" type="get">
113 <model version="1.0" name="post">
120 <model version="1.0" name="post">
114 <id key="id1" type="ecdsa" local-id="1" />
121 <id key="id1" type="ecdsa" local-id="1" />
115 <id key="id1" type="ecdsa" local-id="2" />
122 <id key="id1" type="ecdsa" local-id="2" />
116 </model>
123 </model>
117 </request>
124 </request>
118
125
119 Id consists of a key, key type and local id. This key is used for signing and
126 Id consists of a key, key type and local id. This key is used for signing and
120 validating of data in the model content.
127 validating of data in the model content.
121
128
122 Sample response:
129 Sample response:
123
130
124 <?xml version="1.1" encoding="UTF-8" ?>
131 <?xml version="1.1" encoding="UTF-8" ?>
125 <response>
132 <response>
126 <!--
133 <!--
127 Valid statuses are 'success' and 'error'.
134 Valid statuses are 'success' and 'error'.
128 -->
135 -->
129 <status>success</status>
136 <status>success</status>
130 <models>
137 <models>
131 <model name="post">
138 <model name="post">
132 <!--
139 <!--
133 Content tag is the data that is signed by signatures and must
140 Content tag is the data that is signed by signatures and must
134 not be changed for the post from other node.
141 not be changed for the post from other node.
135 -->
142 -->
136 <content>
143 <content>
137 <id key="id1" type="ecdsa" local-id="1" />
144 <id key="id1" type="ecdsa" local-id="1" />
138 <title>13</title>
145 <title>13</title>
139 <text>Thirteen</text>
146 <text>Thirteen</text>
140 <thread><id key="id1" type="ecdsa" local-id="2" /></thread>
147 <thread><id key="id1" type="ecdsa" local-id="2" /></thread>
141 <pub-time>12</pub-time>
148 <pub-time>12</pub-time>
142 <!--
149 <!--
143 Images are saved as attachments and included in the
150 Images are saved as attachments and included in the
144 signature.
151 signature.
145 -->
152 -->
146 <attachments>
153 <attachments>
147 <attachment mimetype="image/png" id-type="md5">TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5I</attachment>
154 <attachment mimetype="image/png" id-type="md5">TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5I</attachment>
148 </attachments>
155 </attachments>
149 </content>
156 </content>
150 <!--
157 <!--
151 There can be several signatures for one model. At least one
158 There can be several signatures for one model. At least one
152 signature must be made with the key used in global ID.
159 signature must be made with the key used in global ID.
153 -->
160 -->
154 <signatures>
161 <signatures>
155 <signature key="id1" type="ecdsa" value="dhefhtreh" />
162 <signature key="id1" type="ecdsa" value="dhefhtreh" />
156 <signature key="id45" type="ecdsa" value="dsgfgdhefhtreh" />
163 <signature key="id45" type="ecdsa" value="dsgfgdhefhtreh" />
157 </signatures>
164 </signatures>
158 <attachment-refs>
165 <attachment-refs>
159 <attachment-ref ref="TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0"
166 <attachment-ref ref="TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0"
160 url="/media/images/12345.png" />
167 url="/media/images/12345.png" />
161 </attachment-refs>
168 </attachment-refs>
162 </model>
169 </model>
163 <model name="post">
170 <model name="post">
164 <content>
171 <content>
165 <id key="id1" type="ecdsa" local-id="id2" />
172 <id key="id1" type="ecdsa" local-id="id2" />
166 <title>13</title>
173 <title>13</title>
167 <text>Thirteen</text>
174 <text>Thirteen</text>
168 <pub-time>12</pub-time>
175 <pub-time>12</pub-time>
169 <edit-time>13</edit-time>
176 <edit-time>13</edit-time>
170 <tags>
177 <tags>
171 <tag>tag1</tag>
178 <tag>tag1</tag>
172 </tags>
179 </tags>
173 </content>
180 </content>
174 <signatures>
181 <signatures>
175 <signature key="id2" type="ecdsa" value="dehdfh" />
182 <signature key="id2" type="ecdsa" value="dehdfh" />
176 </signatures>
183 </signatures>
177 </model>
184 </model>
178 </models>
185 </models>
179 </response>
186 </response>
180
187
181 ### 3.1.3 put ###
188 ### 3.1.3 put ###
182
189
183 "put" gives a model to the given node (you have no guarantee the node takes
190 "put" gives a model to the given node (you have no guarantee the node takes
184 it, consider you are just advising the node to take your post). This request
191 it, consider you are just advising the node to take your post). This request
185 type is useful in pool where all the nodes try to duplicate all of their data
192 type is useful in pool where all the nodes try to duplicate all of their data
186 across the pool.
193 across the pool.
187
194
188 ## 3.2 Responses ##
195 ## 3.2 Responses ##
189
196
190 ### 3.2.1 "not supported" ###
197 ### 3.2.1 "not supported" ###
191
198
192 If the request if completely not supported, a "not supported" status will be
199 If the request if completely not supported, a "not supported" status will be
193 returned.
200 returned.
194
201
195 ### 3.2.2 "success" ###
202 ### 3.2.2 "success" ###
196
203
197 "success" status means the request was processed and the result is returned.
204 "success" status means the request was processed and the result is returned.
198
205
199 ### 3.2.3 "error" ###
206 ### 3.2.3 "error" ###
200
207
201 If the server knows for sure that the operation cannot be processed, it sends
208 If the server knows for sure that the operation cannot be processed, it sends
202 the "error" status. Additional tags describing the error may be <description>
209 the "error" status. Additional tags describing the error may be <description>
203 and <stack>.
210 and <stack>.
@@ -1,30 +1,31 b''
1 # 0 Title #
1 # 0 Title #
2
2
3 "post" model reference
3 "post" model reference
4
4
5 # 1 Description #
5 # 1 Description #
6
6
7 "post" is a model that defines an imageboard message, or post.
7 "post" is a model that defines an imageboard message, or post.
8
8
9 # 2 Fields #
9 # 2 Fields #
10
10
11 # 2.1 Mandatory fields #
11 # 2.1 Mandatory fields #
12
12
13 * title -- text field.
13 * title -- text field.
14 * text -- text field.
14 * text -- text field.
15 * pub-time -- timestamp (TBD: Define format).
15 * pub-time -- timestamp (TBD: Define format).
16 * version -- when post content changes, version should be incremented.
16
17
17 # 2.2 Optional fields #
18 # 2.2 Optional fields #
18
19
19 * attachments -- defines attachments such as images.
20 * attachments -- defines attachments such as images.
20 * attachment -- contains and attachment or link to attachment to be downloaded
21 * attachment -- contains and attachment or link to attachment to be downloaded
21 manually. Required attributes are mimetype and name.
22 manually. Required attributes are mimetype and name.
22
23
23 This field is used for the opening post (thread):
24 This field is used for the opening post (thread):
24
25
25 * tags -- text tag name list.
26 * tags -- text tag name list.
26
27
27 This field is used for non-opening post:
28 This field is used for non-opening post:
28
29
29 * thread -- ID of a post model that this post is related to.
30 * thread -- ID of a post model that this post is related to.
30 * tripcode
31 * tripcode
General Comments 0
You need to be logged in to leave comments. Login now