Skip to content

Commit e2f19bb

Browse files
committed
chg: [user-account] add forum support. show user-account threads amd posts
1 parent ba8cf4e commit e2f19bb

11 files changed

Lines changed: 395 additions & 21 deletions

File tree

bin/importer/feeders/Forum_Extractor.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ def _upsert_post(self, post_data, parent_thread):
336336
if image:
337337
user_account.set_icon(image.get_global_id())
338338
self.objs_to_process.add(image)
339+
user_account.add_correlation(parent_thread.type, parent_thread.subtype, parent_thread.id)
339340
return post
340341

341342
def _create_image_from_har_url(self, url, date, obj):

bin/lib/ail_core.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ def get_object_all_subtypes(obj_type): # TODO Dynamic subtype
114114
chat_protocols.add('telegram')
115115
return sorted(chat_protocols)
116116
if obj_type == 'user-account':
117-
return r_object.smembers(f'all_chat:subtypes')
117+
# return r_object.smembers(f'all_chat:subtypes')
118+
return r_object.smembers(f'all_user-account:subtypes')
118119
return []
119120

120121
def get_default_correlation_objects():

bin/lib/correlations_engine.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
"favicon": ["domain", "item"], # TODO Decoded
6969
"forum": ["post", "subforum", "user-account"],
7070
"subforum": ["forum", "subforum", "forum-thread"],
71-
"forum-thread": ["subforum", "post"],
71+
"forum-thread": ["subforum", "post", "user-account"],
7272
# TODO Extend to detection of text -> same as message
7373
"post": ["forum", "forum-thread", "image", "user-account", *COMMON_TEXT_OBJECT_CORRELATIONS],
7474
"file-name": ["chat", "item", "message", "pdf"],
@@ -86,7 +86,7 @@
8686
"screenshot": ["barcode", "domain", "item", "qrcode"],
8787
"ssh-key": ["domain", "ip"],
8888
"title": ["domain", "item"],
89-
"user-account": ["chat", "chat-subchannel", "chat-thread", "forum", "image", "message", "ocr", "post", "username"],
89+
"user-account": ["chat", "chat-subchannel", "chat-thread", "forum", "forum-thread", "image", "message", "ocr", "post", "username"],
9090
"username": ["domain", "item", "message", "user-account"],
9191
}
9292

bin/lib/forums_viewer.py

Lines changed: 154 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
# config_loader = ConfigLoader()
3131
# config_loader = None
3232

33-
_FORUM_OPTIONS = {'forum_type', 'info', 'name', 'url', 'banner', 'nb_subforums', 'nb_orphan_subforums'}
33+
_FORUM_OPTIONS = {'banner', 'forum_type', 'info', 'name', 'url', 'nb_subforums', 'nb_orphan_subforums', 'svg_icon'}
3434
_SUBFORUM_OPTIONS = {'info', 'url', 'nb_subforums', 'nb_threads'}
3535
_THREAD_OPTIONS = {'name', 'info', 'url', 'flags', 'nb_posts'}
3636
_POST_OPTIONS = {'content', 'images', 'language', 'link', 'reactions', 'state', 'timestamp', 'translation', 'user-account'}
@@ -365,6 +365,159 @@ def _subforum_threads_meta(subforum):
365365
)
366366

367367

368+
def _get_user_account_posts_sorted(user_account):
369+
posts = []
370+
for pid in user_account.get_posts():
371+
_, post_id = pid.split(':', 1)
372+
post = Posts.Post(post_id)
373+
timestamp = post.get_timestamp()
374+
if timestamp and post.exists():
375+
posts.append((post, float(timestamp)))
376+
return sorted(posts, key=lambda post_item: post_item[1], reverse=True)
377+
378+
379+
def _paginate_items(items, page=1, nb=50):
380+
try:
381+
page = int(page)
382+
except (TypeError, ValueError):
383+
page = 1
384+
try:
385+
nb = int(nb)
386+
except (TypeError, ValueError):
387+
nb = 50
388+
if page < 1:
389+
page = 1
390+
if nb < 1:
391+
nb = 50
392+
total = len(items)
393+
nb_pages = int(total / nb)
394+
if total and total % nb:
395+
nb_pages += 1
396+
if not nb_pages:
397+
nb_pages = 1
398+
if page > nb_pages:
399+
page = nb_pages
400+
start = (page - 1) * nb
401+
end = min(start + nb, total)
402+
return items[start:end], {'nb': nb, 'page': page, 'nb_pages': nb_pages, 'total': total, 'nb_first': start + 1 if total else 0, 'nb_last': end}
403+
404+
405+
def _posts_to_date_dict(post_items, translation_target=None):
406+
posts_by_date = {}
407+
for post, timestamp in post_items:
408+
meta = post.get_meta(_POST_OPTIONS, translation_target=translation_target, flask_context=True)
409+
date_day = Date.get_utc_date_from_timestamp(timestamp, separator='/')
410+
posts_by_date.setdefault(date_day, []).append(meta)
411+
return posts_by_date
412+
413+
414+
def get_user_account_threads_meta(user_account):
415+
threads = []
416+
for thread_str in user_account.get_forum_threads():
417+
thread_subtype, thread_id = thread_str.split(':', 1)
418+
thread = ForumThreads.ForumThread(thread_id, thread_subtype)
419+
thread_meta = _thread_meta(thread) if thread.exists() else {'type': 'forum-thread', 'subtype': thread_subtype, 'id': thread_id}
420+
user_thread_posts = user_account.get_correlation_iter_obj(thread, 'post')
421+
thread_meta['nb_posts'] = len(user_thread_posts)
422+
first_post_timestamp = None
423+
last_post_timestamp = None
424+
for post_id in user_thread_posts:
425+
timestamp = Posts.Post(post_id).get_timestamp()
426+
if not timestamp:
427+
continue
428+
timestamp = float(timestamp)
429+
first_post_timestamp = timestamp if first_post_timestamp is None else min(first_post_timestamp, timestamp)
430+
last_post_timestamp = timestamp if last_post_timestamp is None else max(last_post_timestamp, timestamp)
431+
thread_meta['first_post_timestamp'] = first_post_timestamp or 0
432+
thread_meta['last_post_timestamp'] = last_post_timestamp or 0
433+
if first_post_timestamp:
434+
thread_meta['first_post_date'] = Date.get_utc_datetime_from_timestamp(first_post_timestamp)
435+
else:
436+
thread_meta['first_post_date'] = None
437+
if last_post_timestamp:
438+
thread_meta['last_post_date'] = Date.get_utc_datetime_from_timestamp(last_post_timestamp)
439+
else:
440+
thread_meta['last_post_date'] = None
441+
threads.append(thread_meta)
442+
return sorted(threads, key=lambda thread: thread['last_post_timestamp'], reverse=True)
443+
444+
445+
def get_user_account_nb_all_week_posts(user_account):
446+
week = {day: {hour: 0 for hour in range(24)} for day in ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']}
447+
for post_gid in user_account.get_posts():
448+
_, post_id = post_gid.split(':', 1)
449+
timestamp = Posts.Post(post_id).get_timestamp()
450+
if not timestamp:
451+
continue
452+
weekday, hour = Date.get_utc_weekday_hour_from_timestamp(timestamp)
453+
week[weekday][hour] += 1
454+
stats = []
455+
for nb_day, day in enumerate(week):
456+
for hour in week[day]:
457+
stats.append({'date': day, 'day': nb_day, 'hour': hour, 'count': week[day][hour]})
458+
return stats
459+
460+
461+
def get_user_account_nb_year_posts(user_account, year):
462+
nb_year = {}
463+
nb_max = 0
464+
start = Date.convert_str_date_to_epoch(f'{year}0101')
465+
end = Date.convert_str_date_to_epoch_end(f'{year}1231')
466+
for post_gid in user_account.get_posts():
467+
_, post_id = post_gid.split(':', 1)
468+
timestamp = Posts.Post(post_id).get_timestamp()
469+
if not timestamp:
470+
continue
471+
timestamp = int(float(timestamp))
472+
if start <= timestamp <= end:
473+
date = Date.get_utc_date_from_timestamp(timestamp, separator='-')
474+
nb_year[date] = nb_year.get(date, 0) + 1
475+
nb_max = max(nb_max, nb_year[date])
476+
return nb_max, nb_year
477+
478+
479+
def api_get_user_account(user_id, forum_id, translation_target=None):
480+
user_account = UsersAccount.UserAccount(user_id, forum_id)
481+
if not user_account.exists():
482+
return {"status": "error", "reason": "Unknown user-account"}, 404
483+
meta = user_account.get_meta({'forums', 'icon', 'info', 'translation', 'username', 'usernames', 'username_meta', 'years', 'nb_posts'}, translation_target=translation_target)
484+
forum = Forums.Forum(forum_id)
485+
meta['forum'] = forum.get_meta(_FORUM_OPTIONS, flask_context=True) if forum.exists() else None
486+
meta['threads'] = get_user_account_threads_meta(user_account)
487+
return meta, 200
488+
489+
490+
def api_get_user_account_posts(user_id, forum_id, page=1, nb=50, translation_target=None):
491+
user_account = UsersAccount.UserAccount(user_id, forum_id)
492+
if not user_account.exists():
493+
return {"status": "error", "reason": "Unknown user-account"}, 404
494+
post_items, pagination = _paginate_items(_get_user_account_posts_sorted(user_account), page=page, nb=nb)
495+
meta = user_account.get_meta({'icon', 'info', 'translation', 'username', 'usernames', 'username_meta'}, translation_target=translation_target)
496+
forum = Forums.Forum(forum_id)
497+
meta['forum'] = forum.get_meta(_FORUM_OPTIONS, flask_context=True) if forum.exists() else None
498+
return {'user-account': meta, 'posts': _posts_to_date_dict(post_items, translation_target=translation_target), 'pagination': pagination}, 200
499+
500+
501+
def api_get_user_account_nb_all_week_posts(user_id, forum_id):
502+
user_account = UsersAccount.UserAccount(user_id, forum_id)
503+
if not user_account.exists():
504+
return {"status": "error", "reason": "Unknown user-account"}, 404
505+
return get_user_account_nb_all_week_posts(user_account), 200
506+
507+
508+
def api_get_user_account_nb_year_posts(user_id, forum_id, year):
509+
user_account = UsersAccount.UserAccount(user_id, forum_id)
510+
if not user_account.exists():
511+
return {"status": "error", "reason": "Unknown user-account"}, 404
512+
if not year or year == 'null':
513+
years = user_account.get_years()
514+
year = years[-1] if years else int(Date.get_current_year())
515+
else:
516+
year = int(year)
517+
nb_max, nb = get_user_account_nb_year_posts(user_account, year)
518+
return {'max': nb_max, 'year': year, 'nb': [[date, nb[date]] for date in nb]}, 200
519+
520+
368521
def get_forums():
369522
"""Return metadata for all imported Forum objects."""
370523
forums = []

bin/lib/objects/Posts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def get_thread(self):
6868
def get_thread_sub_id(self):
6969
thread = self.get_correlation('forum-thread')
7070
if thread.get('forum-thread'):
71-
return thread["forum-thread"].pop()
71+
return thread["forum-thread"].pop().split(':', 1)[1]
7272
return None
7373

7474
def get_date(self):

bin/lib/objects/UsersAccount.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,18 @@ def get_chats(self):
145145
def get_nb_chats(self):
146146
return self.get_nb_correlation('chat')
147147

148+
def get_forum(self):
149+
return self.get_correlation('forum').get('forum', set()).pop()
150+
151+
def get_posts(self):
152+
return self.get_correlation('post').get('post', set())
153+
154+
def get_nb_posts(self):
155+
return self.get_nb_correlation('post')
156+
157+
def get_forum_threads(self):
158+
return self.get_correlation('forum-thread').get('forum-thread', set())
159+
148160
def get_chat_subchannels(self):
149161
chats = self.get_correlation('chat-subchannel')['chat-subchannel']
150162
return chats
@@ -228,6 +240,10 @@ def get_meta(self, options=set(), translation_target=None, flask_context=False):
228240
meta['chats'] = self.get_chats()
229241
if 'nb_chats' in options:
230242
meta['nb_chats'] = self.get_nb_chats()
243+
if 'forums' in options:
244+
meta['forums'] = self.get_forum()
245+
if 'nb_posts' in options:
246+
meta['nb_posts'] = self.get_nb_posts()
231247
if 'subchannels' in options:
232248
meta['subchannels'] = self.get_chat_subchannels()
233249
if 'threads' in options:

bin/packages/Date.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,18 @@ def get_utc_datetime_from_timestamp(timestamp):
118118
timestamp = datetime.datetime.fromtimestamp(float(timestamp), datetime.timezone.utc)
119119
return timestamp.strftime('%Y-%m-%d %H:%M:%S')
120120

121+
def get_utc_date_from_timestamp(timestamp, separator=''):
122+
timestamp = datetime.datetime.fromtimestamp(float(timestamp), datetime.timezone.utc)
123+
if separator:
124+
return timestamp.strftime(f'%Y{separator}%m{separator}%d')
125+
return timestamp.strftime('%Y%m%d')
126+
127+
128+
def get_utc_weekday_hour_from_timestamp(timestamp):
129+
timestamp = datetime.datetime.fromtimestamp(float(timestamp), datetime.timezone.utc)
130+
return timestamp.strftime('%a'), timestamp.hour
131+
132+
121133
def get_date_from_timestamp(timestamp):
122134
return datetime.datetime.fromtimestamp(timestamp).strftime('%Y%m%d')
123135

var/www/blueprints/chats_explorer.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
##################################
2222
from lib import ail_core
2323
from lib import chats_viewer
24+
from lib import forums_viewer
2425
from lib import Language
2526
from lib import Tag
2627
from lib import module_extractor
@@ -493,7 +494,12 @@ def objects_user_account():
493494
target = request.args.get('target')
494495
if target == "Don't Translate":
495496
target = None
496-
user_account = chats_viewer.api_get_user_account(user_id, instance_uuid, translation_target=target)
497+
obj = ail_objects.get_object('user-account', instance_uuid, user_id)
498+
is_forum_account = obj.get_forum()
499+
if is_forum_account:
500+
user_account = forums_viewer.api_get_user_account(user_id, instance_uuid, translation_target=target)
501+
else:
502+
user_account = chats_viewer.api_get_user_account(user_id, instance_uuid, translation_target=target)
497503
# print()
498504
# print(user_account[0]['usernames'])
499505
# print()
@@ -505,10 +511,33 @@ def objects_user_account():
505511
translation_languages = Language.get_translation_languages()
506512
languages_stats = chats_viewer.api_get_languages_stats('user-account', instance_uuid, user_id)
507513
lang_endpoint = url_for('chats_explorer.objects_user_account_lang') + f'?subtype={instance_uuid}&id={user_id}&lang='
514+
account_context = 'forum' if is_forum_account else 'chat'
508515
return render_template('user_account.html', meta=user_account, bootstrap_label=bootstrap_label,
509516
ail_tags=Tag.get_modal_add_tags(user_account['id'], user_account['type'], user_account['subtype']),
510517
languages_stats=languages_stats, lang_endpoint=lang_endpoint, all_languages=languages,
511-
translation_languages=translation_languages, translation_target=target)
518+
translation_languages=translation_languages, translation_target=target,
519+
account_context=account_context)
520+
521+
522+
@chats_explorer.route("/objects/user-account/posts", methods=['GET'])
523+
@login_required
524+
@login_read_only
525+
def objects_user_account_posts():
526+
instance_uuid = request.args.get('subtype')
527+
user_id = request.args.get('id')
528+
page = request.args.get('page')
529+
nb = request.args.get('nb')
530+
target = request.args.get('target')
531+
if target == "Don't Translate":
532+
target = None
533+
meta = forums_viewer.api_get_user_account_posts(user_id, instance_uuid, page=page, nb=nb, translation_target=target)
534+
if meta[1] != 200:
535+
return create_json_response(meta[0], meta[1])
536+
languages = Language.get_all_languages()
537+
translation_languages = Language.get_translation_languages()
538+
return render_template('user_account_forum_posts.html', meta=meta[0], bootstrap_label=bootstrap_label,
539+
ail_tags=Tag.get_modal_add_tags(meta[0]['user-account']['id'], meta[0]['user-account']['type'], meta[0]['user-account']['subtype']),
540+
all_languages=languages, translation_languages=translation_languages, translation_target=target)
512541

513542
@chats_explorer.route("/objects/user-account_usernames_timeline_json", methods=['GET']) # TODO API
514543
@login_required
@@ -591,7 +620,11 @@ def objects_user_account_lang():
591620
def user_account_messages_stats_week_all():
592621
instance_uuid = request.args.get('subtype')
593622
user_id = request.args.get('id')
594-
week = chats_viewer.api_get_user_account_nb_all_week_messages(user_id, instance_uuid)
623+
obj = ail_objects.get_object('user-account', instance_uuid, user_id)
624+
if obj.get_forum():
625+
week = forums_viewer.api_get_user_account_nb_all_week_posts(user_id, instance_uuid)
626+
else:
627+
week = chats_viewer.api_get_user_account_nb_all_week_messages(user_id, instance_uuid)
595628
if week[1] != 200:
596629
return create_json_response(week[0], week[1])
597630
else:
@@ -604,7 +637,11 @@ def user_account_messages_stats_year():
604637
instance_uuid = request.args.get('subtype')
605638
user_id = request.args.get('id')
606639
year = request.args.get('year')
607-
stats = chats_viewer.api_get_user_account_nb_year_messages(user_id, instance_uuid, year)
640+
obj = ail_objects.get_object('user-account', instance_uuid, user_id)
641+
if obj.get_forum():
642+
stats = forums_viewer.api_get_user_account_nb_year_posts(user_id, instance_uuid, year)
643+
else:
644+
stats = chats_viewer.api_get_user_account_nb_year_messages(user_id, instance_uuid, year)
608645
if stats[1] != 200:
609646
return create_json_response(stats[0], stats[1])
610647
else:

var/www/templates/chats_explorer/card_user_account.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ <h4 class="text-secondary">{% if meta['username'] %}{{ meta["username"]["id"] }}
2323
<th>ID</th>
2424
<th>First Seen</th>
2525
<th>Last Seen</th>
26-
<th>NB Chats</th>
26+
<th>{% if account_context == 'forum' %}NB Posts{% else %}NB Chats{% endif %}</th>
2727
</tr>
2828
</thead>
2929
<tbody style="font-size: 15px;">
@@ -50,7 +50,7 @@ <h4 class="text-secondary">{% if meta['username'] %}{{ meta["username"]["id"] }}
5050
{{ meta['last_seen'][0:4] }}-{{ meta['last_seen'][4:6] }}-{{ meta['last_seen'][6:8] }}
5151
{% endif %}
5252
</td>
53-
<td>{{ meta['chats'] | length }}</td>
53+
<td>{% if account_context == 'forum' %}{{ meta.get('nb_posts', 0) }}{% else %}{{ meta.get('chats', []) | length }}{% endif %}</td>
5454
</tr>
5555
</tbody>
5656
</table>

0 commit comments

Comments
 (0)