Show More
@@ -0,0 +1,24 b'' | |||||
|
1 | # Generated by Django 2.0.8 on 2018-08-05 17:55 | |||
|
2 | ||||
|
3 | from django.db import migrations, models | |||
|
4 | ||||
|
5 | ||||
|
6 | class Migration(migrations.Migration): | |||
|
7 | ||||
|
8 | dependencies = [ | |||
|
9 | ('boards', '0070_auto_20171225_1149'), | |||
|
10 | ] | |||
|
11 | ||||
|
12 | operations = [ | |||
|
13 | migrations.CreateModel( | |||
|
14 | name='SyncSource', | |||
|
15 | fields=[ | |||
|
16 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | |||
|
17 | ('name', models.TextField()), | |||
|
18 | ('timestamp', models.DateTimeField(blank=True, null=True)), | |||
|
19 | ('url', models.TextField()), | |||
|
20 | ('tags', models.TextField(blank=True)), | |||
|
21 | ('query_split_limit', models.IntegerField()), | |||
|
22 | ], | |||
|
23 | ), | |||
|
24 | ] |
@@ -1,193 +1,197 b'' | |||||
1 | from django.contrib import admin |
|
1 | from django.contrib import admin | |
2 | from django.urls import reverse |
|
2 | from django.urls import reverse | |
3 | from django.utils.translation import ugettext_lazy as _ |
|
3 | from django.utils.translation import ugettext_lazy as _ | |
4 |
|
4 | |||
5 | from boards.models import Post, Tag, Ban, Thread, Banner, Attachment, \ |
|
5 | from boards.models import Post, Tag, Ban, Thread, Banner, Attachment, \ | |
6 | KeyPair, GlobalId, TagAlias, STATUS_ACTIVE |
|
6 | KeyPair, GlobalId, TagAlias, STATUS_ACTIVE | |
7 | from boards.models.attachment import FILE_TYPES_IMAGE, AttachmentSticker, \ |
|
7 | from boards.models.attachment import FILE_TYPES_IMAGE, AttachmentSticker, \ | |
8 | StickerPack |
|
8 | StickerPack | |
9 | from boards.models.source import ThreadSource |
|
9 | from boards.models.source import ThreadSource, SyncSource | |
10 |
|
10 | |||
11 |
|
11 | |||
12 | @admin.register(Post) |
|
12 | @admin.register(Post) | |
13 | class PostAdmin(admin.ModelAdmin): |
|
13 | class PostAdmin(admin.ModelAdmin): | |
14 |
|
14 | |||
15 | list_display = ('id', 'title', 'text', 'poster_ip', 'linked_images', |
|
15 | list_display = ('id', 'title', 'text', 'poster_ip', 'linked_images', | |
16 | 'foreign', 'tags') |
|
16 | 'foreign', 'tags') | |
17 | list_filter = ('pub_time',) |
|
17 | list_filter = ('pub_time',) | |
18 | search_fields = ('id', 'title', 'text', 'poster_ip') |
|
18 | search_fields = ('id', 'title', 'text', 'poster_ip') | |
19 | exclude = ('referenced_posts', 'refmap', 'images', 'global_id') |
|
19 | exclude = ('referenced_posts', 'refmap', 'images', 'global_id') | |
20 | readonly_fields = ('poster_ip', 'thread', 'linked_images', |
|
20 | readonly_fields = ('poster_ip', 'thread', 'linked_images', | |
21 | 'attachments', 'uid', 'url', 'pub_time', 'opening', 'linked_global_id', |
|
21 | 'attachments', 'uid', 'url', 'pub_time', 'opening', 'linked_global_id', | |
22 | 'foreign', 'tags') |
|
22 | 'foreign', 'tags') | |
23 |
|
23 | |||
24 | def ban_poster(self, request, queryset): |
|
24 | def ban_poster(self, request, queryset): | |
25 | bans = 0 |
|
25 | bans = 0 | |
26 | for post in queryset: |
|
26 | for post in queryset: | |
27 | poster_ip = post.poster_ip |
|
27 | poster_ip = post.poster_ip | |
28 | ban, created = Ban.objects.get_or_create(ip=poster_ip) |
|
28 | ban, created = Ban.objects.get_or_create(ip=poster_ip) | |
29 | if created: |
|
29 | if created: | |
30 | bans += 1 |
|
30 | bans += 1 | |
31 | self.message_user(request, _('{} posters were banned').format(bans)) |
|
31 | self.message_user(request, _('{} posters were banned').format(bans)) | |
32 |
|
32 | |||
33 | def ban_latter_with_delete(self, request, queryset): |
|
33 | def ban_latter_with_delete(self, request, queryset): | |
34 | bans = 0 |
|
34 | bans = 0 | |
35 | hidden = 0 |
|
35 | hidden = 0 | |
36 | for post in queryset: |
|
36 | for post in queryset: | |
37 | poster_ip = post.poster_ip |
|
37 | poster_ip = post.poster_ip | |
38 | ban, created = Ban.objects.get_or_create(ip=poster_ip) |
|
38 | ban, created = Ban.objects.get_or_create(ip=poster_ip) | |
39 | if created: |
|
39 | if created: | |
40 | bans += 1 |
|
40 | bans += 1 | |
41 | posts = Post.objects.filter(poster_ip=poster_ip, id__gte=post.id) |
|
41 | posts = Post.objects.filter(poster_ip=poster_ip, id__gte=post.id) | |
42 | hidden += posts.count() |
|
42 | hidden += posts.count() | |
43 | posts.delete() |
|
43 | posts.delete() | |
44 | self.message_user(request, _('{} posters were banned, {} messages were removed.').format(bans, hidden)) |
|
44 | self.message_user(request, _('{} posters were banned, {} messages were removed.').format(bans, hidden)) | |
45 | ban_latter_with_delete.short_description = _('Ban user and delete posts starting from this one and later') |
|
45 | ban_latter_with_delete.short_description = _('Ban user and delete posts starting from this one and later') | |
46 |
|
46 | |||
47 | def linked_images(self, obj: Post): |
|
47 | def linked_images(self, obj: Post): | |
48 | images = obj.attachments.filter(mimetype__in=FILE_TYPES_IMAGE) |
|
48 | images = obj.attachments.filter(mimetype__in=FILE_TYPES_IMAGE) | |
49 | image_urls = ['<a href="{}"><img src="{}" /></a>'.format( |
|
49 | image_urls = ['<a href="{}"><img src="{}" /></a>'.format( | |
50 | reverse('admin:%s_%s_change' % (image._meta.app_label, |
|
50 | reverse('admin:%s_%s_change' % (image._meta.app_label, | |
51 | image._meta.model_name), |
|
51 | image._meta.model_name), | |
52 | args=[image.id]), image.get_thumb_url()) for image in images] |
|
52 | args=[image.id]), image.get_thumb_url()) for image in images] | |
53 | return ', '.join(image_urls) |
|
53 | return ', '.join(image_urls) | |
54 | linked_images.allow_tags = True |
|
54 | linked_images.allow_tags = True | |
55 |
|
55 | |||
56 | def linked_global_id(self, obj: Post): |
|
56 | def linked_global_id(self, obj: Post): | |
57 | global_id = obj.global_id |
|
57 | global_id = obj.global_id | |
58 | if global_id is not None: |
|
58 | if global_id is not None: | |
59 | return '<a href="{}">{}</a>'.format( |
|
59 | return '<a href="{}">{}</a>'.format( | |
60 | reverse('admin:%s_%s_change' % (global_id._meta.app_label, |
|
60 | reverse('admin:%s_%s_change' % (global_id._meta.app_label, | |
61 | global_id._meta.model_name), |
|
61 | global_id._meta.model_name), | |
62 | args=[global_id.id]), str(global_id)) |
|
62 | args=[global_id.id]), str(global_id)) | |
63 | linked_global_id.allow_tags = True |
|
63 | linked_global_id.allow_tags = True | |
64 |
|
64 | |||
65 | def tags(self, obj: Post): |
|
65 | def tags(self, obj: Post): | |
66 | return ', '.join([tag.get_name() for tag in obj.get_tags()]) |
|
66 | return ', '.join([tag.get_name() for tag in obj.get_tags()]) | |
67 |
|
67 | |||
68 | def save_model(self, request, obj, form, change): |
|
68 | def save_model(self, request, obj, form, change): | |
69 | obj.save() |
|
69 | obj.save() | |
70 | obj.clear_cache() |
|
70 | obj.clear_cache() | |
71 |
|
71 | |||
72 | def foreign(self, obj: Post): |
|
72 | def foreign(self, obj: Post): | |
73 | return obj is not None and obj.global_id is not None and\ |
|
73 | return obj is not None and obj.global_id is not None and\ | |
74 | not obj.global_id.is_local() |
|
74 | not obj.global_id.is_local() | |
75 |
|
75 | |||
76 | actions = ['ban_poster', 'ban_latter_with_delete'] |
|
76 | actions = ['ban_poster', 'ban_latter_with_delete'] | |
77 |
|
77 | |||
78 |
|
78 | |||
79 | @admin.register(Tag) |
|
79 | @admin.register(Tag) | |
80 | class TagAdmin(admin.ModelAdmin): |
|
80 | class TagAdmin(admin.ModelAdmin): | |
81 | def thread_count(self, obj: Tag) -> int: |
|
81 | def thread_count(self, obj: Tag) -> int: | |
82 | return obj.get_thread_count() |
|
82 | return obj.get_thread_count() | |
83 |
|
83 | |||
84 | def display_children(self, obj: Tag): |
|
84 | def display_children(self, obj: Tag): | |
85 | return ', '.join([str(child) for child in obj.get_children().all()]) |
|
85 | return ', '.join([str(child) for child in obj.get_children().all()]) | |
86 |
|
86 | |||
87 | def name(self, obj: Tag): |
|
87 | def name(self, obj: Tag): | |
88 | return obj.get_name() |
|
88 | return obj.get_name() | |
89 |
|
89 | |||
90 | def save_model(self, request, obj, form, change): |
|
90 | def save_model(self, request, obj, form, change): | |
91 | super().save_model(request, obj, form, change) |
|
91 | super().save_model(request, obj, form, change) | |
92 | for thread in obj.get_threads().all(): |
|
92 | for thread in obj.get_threads().all(): | |
93 | thread.refresh_tags() |
|
93 | thread.refresh_tags() | |
94 |
|
94 | |||
95 | list_display = ('name', 'thread_count', 'display_children') |
|
95 | list_display = ('name', 'thread_count', 'display_children') | |
96 | search_fields = ('id',) |
|
96 | search_fields = ('id',) | |
97 | readonly_fields = ('name',) |
|
97 | readonly_fields = ('name',) | |
98 |
|
98 | |||
99 |
|
99 | |||
100 | @admin.register(TagAlias) |
|
100 | @admin.register(TagAlias) | |
101 | class TagAliasAdmin(admin.ModelAdmin): |
|
101 | class TagAliasAdmin(admin.ModelAdmin): | |
102 | list_display = ('locale', 'name', 'parent') |
|
102 | list_display = ('locale', 'name', 'parent') | |
103 | list_filter = ('locale',) |
|
103 | list_filter = ('locale',) | |
104 | search_fields = ('name',) |
|
104 | search_fields = ('name',) | |
105 |
|
105 | |||
106 |
|
106 | |||
107 | @admin.register(Thread) |
|
107 | @admin.register(Thread) | |
108 | class ThreadAdmin(admin.ModelAdmin): |
|
108 | class ThreadAdmin(admin.ModelAdmin): | |
109 |
|
109 | |||
110 | def title(self, obj: Thread) -> str: |
|
110 | def title(self, obj: Thread) -> str: | |
111 | return obj.get_opening_post().get_title() |
|
111 | return obj.get_opening_post().get_title() | |
112 |
|
112 | |||
113 | def reply_count(self, obj: Thread) -> int: |
|
113 | def reply_count(self, obj: Thread) -> int: | |
114 | return obj.get_reply_count() |
|
114 | return obj.get_reply_count() | |
115 |
|
115 | |||
116 | def ip(self, obj: Thread): |
|
116 | def ip(self, obj: Thread): | |
117 | return obj.get_opening_post().poster_ip |
|
117 | return obj.get_opening_post().poster_ip | |
118 |
|
118 | |||
119 | def display_tags(self, obj: Thread): |
|
119 | def display_tags(self, obj: Thread): | |
120 | return ', '.join([str(tag) for tag in obj.get_tags().all()]) |
|
120 | return ', '.join([str(tag) for tag in obj.get_tags().all()]) | |
121 |
|
121 | |||
122 | def op(self, obj: Thread): |
|
122 | def op(self, obj: Thread): | |
123 | return obj.get_opening_post_id() |
|
123 | return obj.get_opening_post_id() | |
124 |
|
124 | |||
125 | # Save parent tags when editing tags |
|
125 | # Save parent tags when editing tags | |
126 | def save_related(self, request, form, formsets, change): |
|
126 | def save_related(self, request, form, formsets, change): | |
127 | super().save_related(request, form, formsets, change) |
|
127 | super().save_related(request, form, formsets, change) | |
128 | form.instance.refresh_tags() |
|
128 | form.instance.refresh_tags() | |
129 |
|
129 | |||
130 | def save_model(self, request, obj, form, change): |
|
130 | def save_model(self, request, obj, form, change): | |
131 | op = obj.get_opening_post() |
|
131 | op = obj.get_opening_post() | |
132 | obj.save() |
|
132 | obj.save() | |
133 | op.clear_cache() |
|
133 | op.clear_cache() | |
134 |
|
134 | |||
135 | list_display = ('id', 'op', 'title', 'reply_count', 'status', 'ip', |
|
135 | list_display = ('id', 'op', 'title', 'reply_count', 'status', 'ip', | |
136 | 'display_tags') |
|
136 | 'display_tags') | |
137 | list_filter = ('bump_time', 'status') |
|
137 | list_filter = ('bump_time', 'status') | |
138 | search_fields = ('id', 'title') |
|
138 | search_fields = ('id', 'title') | |
139 | filter_horizontal = ('tags',) |
|
139 | filter_horizontal = ('tags',) | |
140 |
|
140 | |||
141 |
|
141 | |||
142 | @admin.register(KeyPair) |
|
142 | @admin.register(KeyPair) | |
143 | class KeyPairAdmin(admin.ModelAdmin): |
|
143 | class KeyPairAdmin(admin.ModelAdmin): | |
144 | list_display = ('public_key', 'primary') |
|
144 | list_display = ('public_key', 'primary') | |
145 | list_filter = ('primary',) |
|
145 | list_filter = ('primary',) | |
146 | search_fields = ('public_key',) |
|
146 | search_fields = ('public_key',) | |
147 |
|
147 | |||
148 |
|
148 | |||
149 | @admin.register(Ban) |
|
149 | @admin.register(Ban) | |
150 | class BanAdmin(admin.ModelAdmin): |
|
150 | class BanAdmin(admin.ModelAdmin): | |
151 | list_display = ('ip', 'can_read') |
|
151 | list_display = ('ip', 'can_read') | |
152 | list_filter = ('can_read',) |
|
152 | list_filter = ('can_read',) | |
153 | search_fields = ('ip',) |
|
153 | search_fields = ('ip',) | |
154 |
|
154 | |||
155 |
|
155 | |||
156 | @admin.register(Banner) |
|
156 | @admin.register(Banner) | |
157 | class BannerAdmin(admin.ModelAdmin): |
|
157 | class BannerAdmin(admin.ModelAdmin): | |
158 | list_display = ('title', 'text') |
|
158 | list_display = ('title', 'text') | |
159 |
|
159 | |||
160 |
|
160 | |||
161 | @admin.register(Attachment) |
|
161 | @admin.register(Attachment) | |
162 | class AttachmentAdmin(admin.ModelAdmin): |
|
162 | class AttachmentAdmin(admin.ModelAdmin): | |
163 | list_display = ('__str__', 'mimetype', 'file', 'url') |
|
163 | list_display = ('__str__', 'mimetype', 'file', 'url') | |
164 |
|
164 | |||
165 |
|
165 | |||
166 | @admin.register(AttachmentSticker) |
|
166 | @admin.register(AttachmentSticker) | |
167 | class AttachmentStickerAdmin(admin.ModelAdmin): |
|
167 | class AttachmentStickerAdmin(admin.ModelAdmin): | |
168 | search_fields = ('name',) |
|
168 | search_fields = ('name',) | |
169 |
|
169 | |||
170 |
|
170 | |||
171 | @admin.register(StickerPack) |
|
171 | @admin.register(StickerPack) | |
172 | class StickerPackAdmin(admin.ModelAdmin): |
|
172 | class StickerPackAdmin(admin.ModelAdmin): | |
173 | search_fields = ('name',) |
|
173 | search_fields = ('name',) | |
174 |
|
174 | |||
175 |
|
175 | |||
176 | @admin.register(GlobalId) |
|
176 | @admin.register(GlobalId) | |
177 | class GlobalIdAdmin(admin.ModelAdmin): |
|
177 | class GlobalIdAdmin(admin.ModelAdmin): | |
178 | def is_linked(self, obj): |
|
178 | def is_linked(self, obj): | |
179 | return Post.objects.filter(global_id=obj).exists() |
|
179 | return Post.objects.filter(global_id=obj).exists() | |
180 |
|
180 | |||
181 | list_display = ('__str__', 'is_linked',) |
|
181 | list_display = ('__str__', 'is_linked',) | |
182 | readonly_fields = ('content',) |
|
182 | readonly_fields = ('content',) | |
183 |
|
183 | |||
184 |
|
184 | |||
185 | @admin.register(ThreadSource) |
|
185 | @admin.register(ThreadSource) | |
186 | class ThreadSourceAdmin(admin.ModelAdmin): |
|
186 | class ThreadSourceAdmin(admin.ModelAdmin): | |
187 | search_fields = ('name', 'source') |
|
187 | search_fields = ('name', 'source') | |
188 |
|
188 | |||
189 | def formfield_for_foreignkey(self, db_field, request, **kwargs): |
|
189 | def formfield_for_foreignkey(self, db_field, request, **kwargs): | |
190 | if db_field.name == 'thread': |
|
190 | if db_field.name == 'thread': | |
191 | kwargs['queryset'] = Thread.objects.filter(status=STATUS_ACTIVE) |
|
191 | kwargs['queryset'] = Thread.objects.filter(status=STATUS_ACTIVE) | |
192 | return super().formfield_for_foreignkey(db_field, request, **kwargs) |
|
192 | return super().formfield_for_foreignkey(db_field, request, **kwargs) | |
193 |
|
193 | |||
|
194 | ||||
|
195 | @admin.register(SyncSource) | |||
|
196 | class SyncSourceAdmin(admin.ModelAdmin): | |||
|
197 | search_fields = ('name',) No newline at end of file |
@@ -1,140 +1,13 b'' | |||||
1 | import re |
|
1 | from django.core.management import BaseCommand | |
2 | import logging |
|
|||
3 | import xml.etree.ElementTree as ET |
|
|||
4 |
|
2 | |||
5 | import httplib2 |
|
3 | from boards.models.source import SyncSource | |
6 | from django.core.management import BaseCommand |
|
|||
7 | from django.utils.dateparse import parse_datetime |
|
|||
8 |
|
||||
9 | from boards.models import GlobalId, KeyPair |
|
|||
10 | from boards.models.post.sync import SyncManager, TAG_ID, TAG_UPDATE_TIME |
|
|||
11 |
|
4 | |||
12 | __author__ = 'neko259' |
|
5 | __author__ = 'neko259' | |
13 |
|
6 | |||
14 |
|
7 | |||
15 | REGEX_GLOBAL_ID = re.compile(r'(\w+)::([\w\+/]+)::(\d+)') |
|
|||
16 |
|
||||
17 |
|
||||
18 | class Command(BaseCommand): |
|
8 | class Command(BaseCommand): | |
19 | help = 'Send a sync or get request to the server.' |
|
9 | help = 'Send a sync or get request to the servers.' | |
20 |
|
||||
21 | def add_arguments(self, parser): |
|
|||
22 | parser.add_argument('url', type=str, help='Server root url') |
|
|||
23 | parser.add_argument('--global-id', type=str, default='', |
|
|||
24 | help='Post global ID') |
|
|||
25 | parser.add_argument('--split-query', type=int, default=1, |
|
|||
26 | help='Split GET query into separate by the given' |
|
|||
27 | ' number of posts in one') |
|
|||
28 | parser.add_argument('--thread', type=int, |
|
|||
29 | help='Get posts of one specific thread') |
|
|||
30 | parser.add_argument('--tags', type=str, |
|
|||
31 | help='Get posts of the tags, comma-separated') |
|
|||
32 | parser.add_argument('--time-from', type=str, |
|
|||
33 | help='Get posts from the given timestamp') |
|
|||
34 |
|
10 | |||
35 | def handle(self, *args, **options): |
|
11 | def handle(self, *args, **options): | |
36 | logger = logging.getLogger('boards.sync') |
|
12 | for source in SyncSource.objects.all(): | |
37 |
|
13 | source.run_sync() | ||
38 | url = options.get('url') |
|
|||
39 |
|
||||
40 | list_url = url + 'api/sync/list/' |
|
|||
41 | get_url = url + 'api/sync/get/' |
|
|||
42 | file_url = url[:-1] |
|
|||
43 |
|
||||
44 | global_id_str = options.get('global_id') |
|
|||
45 | if global_id_str: |
|
|||
46 | match = REGEX_GLOBAL_ID.match(global_id_str) |
|
|||
47 | if match: |
|
|||
48 | key_type = match.group(1) |
|
|||
49 | key = match.group(2) |
|
|||
50 | local_id = match.group(3) |
|
|||
51 |
|
||||
52 | global_id = GlobalId(key_type=key_type, key=key, |
|
|||
53 | local_id=local_id) |
|
|||
54 |
|
||||
55 | xml = SyncManager.generate_request_get([global_id]) |
|
|||
56 | h = httplib2.Http() |
|
|||
57 | response, content = h.request(get_url, method="POST", body=xml) |
|
|||
58 |
|
||||
59 | SyncManager.parse_response_get(content, file_url) |
|
|||
60 | else: |
|
|||
61 | raise Exception('Invalid global ID') |
|
|||
62 | else: |
|
|||
63 | logger.info('Running LIST request...') |
|
|||
64 | h = httplib2.Http() |
|
|||
65 |
|
||||
66 | tags = [] |
|
|||
67 | tags_str = options.get('tags') |
|
|||
68 | if tags_str: |
|
|||
69 | tags = tags_str.split(',') |
|
|||
70 |
|
||||
71 | timestamp_str = options.get('time_from') |
|
|||
72 | timestamp = None |
|
|||
73 | if timestamp_str: |
|
|||
74 | timestamp = parse_datetime(timestamp_str) |
|
|||
75 | if not timestamp: |
|
|||
76 | raise Exception('Timestamp {} cannot be parsed'.format( |
|
|||
77 | timestamp_str)) |
|
|||
78 |
|
||||
79 | xml = SyncManager.generate_request_list( |
|
|||
80 | opening_post=options.get('thread'), tags=tags, |
|
|||
81 | timestamp_from=timestamp).encode() |
|
|||
82 | response, content = h.request(list_url, method="POST", body=xml) |
|
|||
83 | if response.status != 200: |
|
|||
84 | raise Exception('Server returned error {}'.format(response.status)) |
|
|||
85 |
|
||||
86 | logger.info('Processing response...') |
|
|||
87 |
|
||||
88 | root = ET.fromstring(content) |
|
|||
89 | status = root.findall('status')[0].text |
|
|||
90 | if status == 'success': |
|
|||
91 | ids_to_sync = list() |
|
|||
92 |
|
||||
93 | models = root.findall('models')[0] |
|
|||
94 | for model in models: |
|
|||
95 | self.add_to_sync_list(ids_to_sync, logger, model) |
|
|||
96 | logger.info('Starting sync...') |
|
|||
97 |
|
||||
98 | if len(ids_to_sync) > 0: |
|
|||
99 | limit = options.get('split_query', len(ids_to_sync)) |
|
|||
100 | for offset in range(0, len(ids_to_sync), limit): |
|
|||
101 | xml = SyncManager.generate_request_get(ids_to_sync[offset:offset + limit]) |
|
|||
102 | h = httplib2.Http() |
|
|||
103 | logger.info('Running GET request...') |
|
|||
104 | response, content = h.request(get_url, method="POST", body=xml) |
|
|||
105 | logger.info('Processing response...') |
|
|||
106 |
|
||||
107 | SyncManager.parse_response_get(content, file_url) |
|
|||
108 |
|
||||
109 | logger.info('Sync completed successfully') |
|
|||
110 | else: |
|
|||
111 | logger.info('Nothing to get, everything synced') |
|
|||
112 | else: |
|
|||
113 | raise Exception('Invalid response status') |
|
|||
114 |
|
||||
115 | def add_to_sync_list(self, ids_to_sync, logger, model): |
|
|||
116 | tag_id = model.find(TAG_ID) |
|
|||
117 | global_id, exists = GlobalId.from_xml_element(tag_id) |
|
|||
118 | from_this_board = self._is_from_this_board(global_id) |
|
|||
119 | if from_this_board: |
|
|||
120 | # If the post originates from this board, no need to process |
|
|||
121 | # it again, nobody else could modify it |
|
|||
122 | logger.debug('NO SYNC Processed post {}'.format(global_id)) |
|
|||
123 | else: |
|
|||
124 | tag_update_time = model.find(TAG_UPDATE_TIME) |
|
|||
125 | if tag_update_time: |
|
|||
126 | update_time = tag_update_time.text |
|
|||
127 | else: |
|
|||
128 | update_time = None |
|
|||
129 | if not exists or update_time is None or global_id.post.last_edit_time < parse_datetime( |
|
|||
130 | update_time): |
|
|||
131 | logger.debug('SYNC Processed post {}'.format(global_id)) |
|
|||
132 | ids_to_sync.append(global_id) |
|
|||
133 | else: |
|
|||
134 | logger.debug('NO SYNC Processed post {}'.format(global_id)) |
|
|||
135 |
|
||||
136 | def _is_from_this_board(self, global_id): |
|
|||
137 | from_this_board = KeyPair.objects.filter( |
|
|||
138 | key_type=global_id.key_type, |
|
|||
139 | public_key=global_id.key).exists() |
|
|||
140 | return from_this_board |
|
@@ -1,85 +1,195 b'' | |||||
1 | import feedparser |
|
1 | import feedparser | |
|
2 | import httplib2 | |||
2 | import logging |
|
3 | import logging | |
3 | import calendar |
|
4 | import calendar | |
|
5 | import xml.etree.ElementTree as ET | |||
4 |
|
6 | |||
5 | from time import mktime |
|
7 | from time import mktime | |
6 | from datetime import datetime |
|
8 | from datetime import datetime | |
7 |
|
9 | |||
8 | from django.db import models, transaction |
|
10 | from django.db import models, transaction | |
9 | from django.utils.dateparse import parse_datetime |
|
11 | from django.utils.dateparse import parse_datetime | |
10 | from django.utils.timezone import utc |
|
12 | from django.utils.timezone import utc | |
11 | from django.utils import timezone |
|
13 | from django.utils import timezone | |
12 | from django.utils.html import strip_tags |
|
14 | from django.utils.html import strip_tags | |
13 |
|
15 | |||
14 | from boards.models import Post |
|
16 | from boards.models import Post, GlobalId, KeyPair | |
15 | from boards.models.post import TITLE_MAX_LENGTH |
|
17 | from boards.models.post import TITLE_MAX_LENGTH | |
|
18 | from boards.models.post.sync import SyncManager, TAG_ID, TAG_UPDATE_TIME | |||
16 | from boards.settings import SECTION_EXTERNAL |
|
19 | from boards.settings import SECTION_EXTERNAL | |
17 | from boards.utils import get_tripcode_from_text |
|
20 | from boards.utils import get_tripcode_from_text | |
18 | from boards import settings |
|
21 | from boards import settings | |
19 |
|
22 | |||
|
23 | DELIMITER_TAGS = ',' | |||
20 |
|
24 | |||
21 | SOURCE_TYPE_MAX_LENGTH = 100 |
|
25 | SOURCE_TYPE_MAX_LENGTH = 100 | |
22 | SOURCE_TYPE_RSS = 'RSS' |
|
26 | SOURCE_TYPE_RSS = 'RSS' | |
23 | TYPE_CHOICES = ( |
|
27 | TYPE_CHOICES = ( | |
24 | (SOURCE_TYPE_RSS, SOURCE_TYPE_RSS), |
|
28 | (SOURCE_TYPE_RSS, SOURCE_TYPE_RSS), | |
25 | ) |
|
29 | ) | |
26 |
|
30 | |||
27 |
|
31 | |||
28 | class ThreadSource(models.Model): |
|
32 | class ThreadSource(models.Model): | |
29 | class Meta: |
|
33 | class Meta: | |
30 | app_label = 'boards' |
|
34 | app_label = 'boards' | |
31 |
|
35 | |||
32 | name = models.TextField() |
|
36 | name = models.TextField() | |
33 | thread = models.ForeignKey('Thread', on_delete=models.CASCADE) |
|
37 | thread = models.ForeignKey('Thread', on_delete=models.CASCADE) | |
34 | timestamp = models.DateTimeField() |
|
38 | timestamp = models.DateTimeField() | |
35 | source = models.TextField() |
|
39 | source = models.TextField() | |
36 | source_type = models.CharField(max_length=SOURCE_TYPE_MAX_LENGTH, |
|
40 | source_type = models.CharField(max_length=SOURCE_TYPE_MAX_LENGTH, | |
37 | choices=TYPE_CHOICES) |
|
41 | choices=TYPE_CHOICES) | |
38 |
|
42 | |||
39 | def __str__(self): |
|
43 | def __str__(self): | |
40 | return self.name |
|
44 | return self.name | |
41 |
|
45 | |||
42 | @transaction.atomic |
|
46 | @transaction.atomic | |
43 | def fetch_latest_posts(self): |
|
47 | def fetch_latest_posts(self): | |
44 | """Creates new posts with the info fetched since the timestamp.""" |
|
48 | """Creates new posts with the info fetched since the timestamp.""" | |
45 | logger = logging.getLogger('boards.source') |
|
49 | logger = logging.getLogger('boards.source') | |
46 |
|
50 | |||
47 | if self.thread.is_archived(): |
|
51 | if self.thread.is_archived(): | |
48 | logger.error('The thread {} is archived, please try another one'.format(self.thread)) |
|
52 | logger.error('The thread {} is archived, please try another one'.format(self.thread)) | |
49 | else: |
|
53 | else: | |
50 | tripcode = get_tripcode_from_text( |
|
54 | tripcode = get_tripcode_from_text( | |
51 | settings.get(SECTION_EXTERNAL, 'SourceFetcherTripcode')) |
|
55 | settings.get(SECTION_EXTERNAL, 'SourceFetcherTripcode')) | |
52 | start_timestamp = self.timestamp |
|
56 | start_timestamp = self.timestamp | |
53 | last_timestamp = start_timestamp |
|
57 | last_timestamp = start_timestamp | |
54 | logger.info('Start timestamp is {}'.format(start_timestamp)) |
|
58 | logger.info('Start timestamp is {}'.format(start_timestamp)) | |
55 | if self.thread.is_bumplimit(): |
|
59 | if self.thread.is_bumplimit(): | |
56 | logger.warn('The thread {} has reached its bumplimit, please create a new one'.format(self.thread)) |
|
60 | logger.warn('The thread {} has reached its bumplimit, please create a new one'.format(self.thread)) | |
57 | if self.source_type == SOURCE_TYPE_RSS: |
|
61 | if self.source_type == SOURCE_TYPE_RSS: | |
58 | feed = feedparser.parse(self.source) |
|
62 | feed = feedparser.parse(self.source) | |
59 | items = sorted(feed.entries, key=lambda entry: entry.published_parsed) |
|
63 | items = sorted(feed.entries, key=lambda entry: entry.published_parsed) | |
60 | for item in items: |
|
64 | for item in items: | |
61 | title = self.strip_title(item.title, TITLE_MAX_LENGTH) |
|
65 | title = self.strip_title(item.title, TITLE_MAX_LENGTH) | |
62 | timestamp = datetime.fromtimestamp(calendar.timegm(item.published_parsed), tz=utc) |
|
66 | timestamp = datetime.fromtimestamp(calendar.timegm(item.published_parsed), tz=utc) | |
63 | if not timestamp: |
|
67 | if not timestamp: | |
64 | logger.error('Invalid timestamp {} for {}'.format(item.published, title)) |
|
68 | logger.error('Invalid timestamp {} for {}'.format(item.published, title)) | |
65 | else: |
|
69 | else: | |
66 | if timestamp > last_timestamp: |
|
70 | if timestamp > last_timestamp: | |
67 | last_timestamp = timestamp |
|
71 | last_timestamp = timestamp | |
68 | if timestamp > start_timestamp: |
|
72 | if timestamp > start_timestamp: | |
69 | Post.objects.create_post(title=title, text=self.parse_text(item.description), |
|
73 | Post.objects.create_post(title=title, text=self.parse_text(item.description), | |
70 | thread=self.thread, file_urls=[item.link], tripcode=tripcode) |
|
74 | thread=self.thread, file_urls=[item.link], tripcode=tripcode) | |
71 | logger.info('Fetched item {} from {} into thread {}'.format( |
|
75 | logger.info('Fetched item {} from {} into thread {}'.format( | |
72 | title, self.name, self.thread)) |
|
76 | title, self.name, self.thread)) | |
73 | logger.info('New timestamp is {}'.format(last_timestamp)) |
|
77 | logger.info('New timestamp is {}'.format(last_timestamp)) | |
74 | self.timestamp = last_timestamp |
|
78 | self.timestamp = last_timestamp | |
75 | self.save(update_fields=['timestamp']) |
|
79 | self.save(update_fields=['timestamp']) | |
76 |
|
80 | |||
77 | def parse_text(self, text): |
|
81 | def parse_text(self, text): | |
78 | return strip_tags(text) |
|
82 | return strip_tags(text) | |
79 |
|
83 | |||
80 | def strip_title(self, title, max_length): |
|
84 | def strip_title(self, title, max_length): | |
81 | result = title |
|
85 | result = title | |
82 | if len(title) > max_length: |
|
86 | if len(title) > max_length: | |
83 | result = title[:max_length - 1] + 'β¦' |
|
87 | result = title[:max_length - 1] + 'β¦' | |
84 | return result |
|
88 | return result | |
85 |
|
89 | |||
|
90 | ||||
|
91 | class SyncSource(models.Model): | |||
|
92 | class Meta: | |||
|
93 | app_label = 'boards' | |||
|
94 | ||||
|
95 | name = models.TextField() | |||
|
96 | timestamp = models.DateTimeField(blank=True, null=True) | |||
|
97 | url = models.TextField() | |||
|
98 | tags = models.TextField(blank=True) | |||
|
99 | query_split_limit = models.IntegerField() | |||
|
100 | ||||
|
101 | def __str__(self): | |||
|
102 | return self.name | |||
|
103 | ||||
|
104 | @transaction.atomic | |||
|
105 | def run_sync(self): | |||
|
106 | logger = logging.getLogger('boards.sync') | |||
|
107 | ||||
|
108 | tags = [] | |||
|
109 | if self.tags: | |||
|
110 | tags = self.tags.split(DELIMITER_TAGS) | |||
|
111 | ||||
|
112 | timestamp = None | |||
|
113 | if self.timestamp: | |||
|
114 | timestamp = self.timestamp | |||
|
115 | ||||
|
116 | new_timestamp = timezone.now() | |||
|
117 | ||||
|
118 | list_url = '{}api/sync/list/'.format(self.url) | |||
|
119 | get_url = '{}api/sync/get/'.format(self.url) | |||
|
120 | file_url = self.url[:-1] | |||
|
121 | ||||
|
122 | xml = SyncManager.generate_request_list( | |||
|
123 | tags=tags, | |||
|
124 | timestamp_from=timestamp).encode() | |||
|
125 | ||||
|
126 | logger.info('Running LIST request for {}...'.format(self.name)) | |||
|
127 | h = httplib2.Http() | |||
|
128 | response, content = h.request(list_url, method="POST", body=xml) | |||
|
129 | if response.status != 200: | |||
|
130 | raise Exception('Server returned error {}'.format(response.status)) | |||
|
131 | ||||
|
132 | logger.info('Processing response...') | |||
|
133 | ||||
|
134 | root = ET.fromstring(content) | |||
|
135 | status = root.findall('status')[0].text | |||
|
136 | if status == 'success': | |||
|
137 | ids_to_sync = list() | |||
|
138 | ||||
|
139 | models = root.findall('models')[0] | |||
|
140 | for model in models: | |||
|
141 | self.add_to_sync_list(ids_to_sync, logger, model) | |||
|
142 | logger.info('Starting sync...') | |||
|
143 | ||||
|
144 | if len(ids_to_sync) > 0: | |||
|
145 | if self.query_split_limit > 0: | |||
|
146 | limit = min(self.query_split_limit, len(ids_to_sync)) | |||
|
147 | else: | |||
|
148 | limit = len(ids_to_sync) | |||
|
149 | ||||
|
150 | for offset in range(0, len(ids_to_sync), limit): | |||
|
151 | xml = SyncManager.generate_request_get( | |||
|
152 | ids_to_sync[offset:offset + limit]) | |||
|
153 | h = httplib2.Http() | |||
|
154 | logger.info('Running GET request...') | |||
|
155 | response, content = h.request(get_url, method="POST", | |||
|
156 | body=xml) | |||
|
157 | logger.info('Processing response...') | |||
|
158 | ||||
|
159 | SyncManager.parse_response_get(content, file_url) | |||
|
160 | ||||
|
161 | logger.info('Sync completed successfully for {}'.format(self.name)) | |||
|
162 | else: | |||
|
163 | logger.info('Nothing to get for {}, everything synced'.format(self.name)) | |||
|
164 | else: | |||
|
165 | raise Exception('Invalid response status') | |||
|
166 | ||||
|
167 | self.timestamp = new_timestamp | |||
|
168 | self.save(update_fields=['timestamp']) | |||
|
169 | ||||
|
170 | def add_to_sync_list(self, ids_to_sync, logger, model): | |||
|
171 | tag_id = model.find(TAG_ID) | |||
|
172 | global_id, exists = GlobalId.from_xml_element(tag_id) | |||
|
173 | from_this_board = self._is_from_this_board(global_id) | |||
|
174 | if from_this_board: | |||
|
175 | # If the post originates from this board, no need to process | |||
|
176 | # it again, nobody else could modify it | |||
|
177 | logger.debug('NO SYNC Processed post {}'.format(global_id)) | |||
|
178 | else: | |||
|
179 | tag_update_time = model.find(TAG_UPDATE_TIME) | |||
|
180 | if tag_update_time: | |||
|
181 | update_time = tag_update_time.text | |||
|
182 | else: | |||
|
183 | update_time = None | |||
|
184 | if not exists or update_time is None or global_id.post.last_edit_time < parse_datetime( | |||
|
185 | update_time): | |||
|
186 | logger.debug('SYNC Processed post {}'.format(global_id)) | |||
|
187 | ids_to_sync.append(global_id) | |||
|
188 | else: | |||
|
189 | logger.debug('NO SYNC Processed post {}'.format(global_id)) | |||
|
190 | ||||
|
191 | def _is_from_this_board(self, global_id): | |||
|
192 | from_this_board = KeyPair.objects.filter( | |||
|
193 | key_type=global_id.key_type, | |||
|
194 | public_key=global_id.key).exists() | |||
|
195 | return from_this_board |
General Comments 0
You need to be logged in to leave comments.
Login now