diff --git a/osinaweb/db.sqlite3 b/osinaweb/db.sqlite3 index 2491d07f..fb41427b 100644 Binary files a/osinaweb/db.sqlite3 and b/osinaweb/db.sqlite3 differ diff --git a/osinaweb/input.css b/osinaweb/input.css index 090b9a05..51022c54 100644 --- a/osinaweb/input.css +++ b/osinaweb/input.css @@ -439,7 +439,6 @@ .onlineDot { width: 15px; height: 15px; - background-color: #1ABC9C; /* Fallback color */ border-radius: 50%; position: relative; diff --git a/osinaweb/osichat/__pycache__/consumers.cpython-310.pyc b/osinaweb/osichat/__pycache__/consumers.cpython-310.pyc index 5bd69a3d..45dbe340 100644 Binary files a/osinaweb/osichat/__pycache__/consumers.cpython-310.pyc and b/osinaweb/osichat/__pycache__/consumers.cpython-310.pyc differ diff --git a/osinaweb/osichat/__pycache__/models.cpython-310.pyc b/osinaweb/osichat/__pycache__/models.cpython-310.pyc index 0f665e1b..17168c84 100644 Binary files a/osinaweb/osichat/__pycache__/models.cpython-310.pyc and b/osinaweb/osichat/__pycache__/models.cpython-310.pyc differ diff --git a/osinaweb/osichat/__pycache__/routing.cpython-310.pyc b/osinaweb/osichat/__pycache__/routing.cpython-310.pyc index 18b6a6f5..7f4adc47 100644 Binary files a/osinaweb/osichat/__pycache__/routing.cpython-310.pyc and b/osinaweb/osichat/__pycache__/routing.cpython-310.pyc differ diff --git a/osinaweb/osichat/consumers.py b/osinaweb/osichat/consumers.py index e9f03e1a..e6bf4e12 100644 --- a/osinaweb/osichat/consumers.py +++ b/osinaweb/osichat/consumers.py @@ -38,11 +38,14 @@ class OsitcomVisitor(WebsocketConsumer): session_id = session_id, ip_address = text_data_json.get('client_ip'), country = text_data_json.get('client_country'), + browser_name = text_data_json.get('browser_name'), + os_name = text_data_json.get('os_name'), ) self.current_log = VisitorLog.objects.create( visitor = self.visitor, referrer = text_data_json.get('referrer'), + title = text_data_json.get('title'), url = text_data_json.get('url'), visit_date = datetime.now() ) @@ -86,7 +89,6 @@ class Osichat(WebsocketConsumer): ) if event_type == 'get_visitors': - print('eeewew') event = { 'type': 'get_visitors_handler', } @@ -154,7 +156,7 @@ class Osichat(WebsocketConsumer): if self.client_type == 'mobile_admin': chat_room_data = model_to_dict(chat_room) self.send(text_data=json.dumps({ - 'event_type': 'new_update', + 'event_type': 'new_chat_update', 'chat_room_data': chat_room_data, }, cls=DjangoJSONEncoder)) else: @@ -168,13 +170,18 @@ class Osichat(WebsocketConsumer): def get_visitors_handler(self, event): - visitors = Visitor.objects.all().order_by('-id') + today = timezone.now().date() + visitors = Visitor.objects.filter(visitorlog__visit_date__date=today).annotate(latest_visit=Max('visitorlog__visit_date')).order_by('-latest_visit') context = { 'visitors': visitors, } if self.client_type == 'mobile_admin': visitors_data = [] for visitor in visitors: + visitors_data.append({ + 'is_online': visitor.is_online, + 'duration': visitor.total_duration, + }) visitor_dict = model_to_dict(visitor) visitor_logs = VisitorLog.objects.filter(visitor=visitor) @@ -193,7 +200,29 @@ class Osichat(WebsocketConsumer): self.send(text_data=json.dumps({ 'event_type': 'get_visitors', 'html': html, - })) + })) + + + def new_visitor_update_handler(self, event): + visitor = get_object_or_404(Visitor, id=event['visitor_id']) + context = { + 'visitor': visitor, + } + if self.client_type == 'mobile_admin': + visitor_data = model_to_dict(visitor) + self.send(text_data=json.dumps({ + 'event_type': 'new_visitor_update', + 'visitor_data': visitor_data, + 'action': event['action'], + }, cls=DjangoJSONEncoder)) + else: + html = render_to_string("chat_templates/partials/new-visitor.html", context=context) + self.send(text_data=json.dumps({ + 'event_type': 'new_visitor_update', + 'visitor_id': visitor.id, + 'action': event['action'], + 'html': html, + })) @@ -209,7 +238,6 @@ class OsitcomChatRoom(WebsocketConsumer): self.visitor = Visitor.objects.filter(session_id=self.session_id).last() if not self.visitor: self.close() - return if self.scope['url_route']['kwargs'].get('chat_id'): #Case where admin is accessing a specific conversation between the conversations of this visior self.chat_room = get_object_or_404(ChatRoom, id=self.scope['url_route']['kwargs'].get('chat_id')) diff --git a/osinaweb/osichat/migrations/0022_visitorlog_title.py b/osinaweb/osichat/migrations/0022_visitorlog_title.py new file mode 100644 index 00000000..7f169dab --- /dev/null +++ b/osinaweb/osichat/migrations/0022_visitorlog_title.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.5 on 2024-08-09 08:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('osichat', '0021_chatroomreview_details'), + ] + + operations = [ + migrations.AddField( + model_name='visitorlog', + name='title', + field=models.CharField(blank=True, max_length=500, null=True), + ), + ] diff --git a/osinaweb/osichat/migrations/0023_visitor_browser_name_visitor_os_name.py b/osinaweb/osichat/migrations/0023_visitor_browser_name_visitor_os_name.py new file mode 100644 index 00000000..7eb396a9 --- /dev/null +++ b/osinaweb/osichat/migrations/0023_visitor_browser_name_visitor_os_name.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.5 on 2024-08-09 09:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('osichat', '0022_visitorlog_title'), + ] + + operations = [ + migrations.AddField( + model_name='visitor', + name='browser_name', + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AddField( + model_name='visitor', + name='os_name', + field=models.CharField(blank=True, max_length=100, null=True), + ), + ] diff --git a/osinaweb/osichat/migrations/__pycache__/0022_visitorlog_title.cpython-310.pyc b/osinaweb/osichat/migrations/__pycache__/0022_visitorlog_title.cpython-310.pyc new file mode 100644 index 00000000..9976a2c4 Binary files /dev/null and b/osinaweb/osichat/migrations/__pycache__/0022_visitorlog_title.cpython-310.pyc differ diff --git a/osinaweb/osichat/migrations/__pycache__/0023_visitor_browser_name_visitor_os_name.cpython-310.pyc b/osinaweb/osichat/migrations/__pycache__/0023_visitor_browser_name_visitor_os_name.cpython-310.pyc new file mode 100644 index 00000000..7eb5ea45 Binary files /dev/null and b/osinaweb/osichat/migrations/__pycache__/0023_visitor_browser_name_visitor_os_name.cpython-310.pyc differ diff --git a/osinaweb/osichat/models.py b/osinaweb/osichat/models.py index 2c4db2d8..c0043a5b 100644 --- a/osinaweb/osichat/models.py +++ b/osinaweb/osichat/models.py @@ -15,19 +15,65 @@ class Visitor(models.Model): name = models.CharField(max_length=200, blank=True, null=True) mobile_number = models.CharField(max_length=10, null=True, blank=True) email = models.CharField(max_length=100, null=True, blank=True) + browser_name = models.CharField(max_length=100, null=True, blank=True) + os_name = models.CharField(max_length=100, null=True, blank=True) @property def flag_image_url(self): flag_url = f"https://osina.ositcom.com/static/images/flags/{self.country.lower()}.svg" return flag_url + @property + def is_online(self): + latest_log = self.visitorlog_set.order_by('-visit_date').first() + if latest_log and latest_log.left_date is None: + return True + return False + @property + def total_duration(self): + total_seconds = 0 + for log in self.visitorlog_set.all(): + end_time = log.left_date if log.left_date else datetime.now(timezone.utc) + total_seconds += (end_time - log.visit_date).total_seconds() + + duration = timedelta(seconds=total_seconds) + hours, remainder = divmod(duration.total_seconds(), 3600) + minutes, seconds = divmod(remainder, 60) + + if hours >= 1: + return f"{int(hours):02}:{int(minutes):02}:{int(seconds):02}" + else: + return f"{int(minutes):02}:{int(seconds):02}" + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + channel_layer = get_channel_layer() + event = { + 'type': 'new_visitor_update_handler', + 'visitor_id': self.id, + 'action': 'new_visitor' + } + async_to_sync(channel_layer.group_send)("osichat", event) class VisitorLog(models.Model): visitor = models.ForeignKey(Visitor, on_delete=models.CASCADE, null=True) url = models.URLField() + title = models.CharField(null=True, blank=True, max_length=500) referrer = models.URLField(null=True, blank=True) visit_date = models.DateTimeField(null=True) left_date = models.DateTimeField(null=True) + def save(self, *args, **kwargs): + if self.left_date: + action = 'end_log' + else: + action = 'new_log' + super().save(*args, **kwargs) + channel_layer = get_channel_layer() + event = { + 'type': 'new_visitor_update_handler', + 'visitor_id': self.visitor.id, + 'action': action + } + async_to_sync(channel_layer.group_send)("osichat", event) class ChatRoom(models.Model): @@ -51,10 +97,7 @@ class ChatRoom(models.Model): return last_updated_time.strftime('%d-%m-%Y') def unread_messages(self, user): return ChatMessage.objects.filter(room=self).exclude(member=user).exclude(chatmessageseen__member=user).count() - - def save(self, *args, **kwargs): - is_new = self.pk is None super().save(*args, **kwargs) channel_layer = get_channel_layer() event = { @@ -98,7 +141,6 @@ class ChatMessage(models.Model): content = models.TextField(null=True, blank=True) date_sent = models.DateTimeField() def save(self, *args, **kwargs): - is_new = self.pk is None super().save(*args, **kwargs) channel_layer = get_channel_layer() event = { diff --git a/osinaweb/osichat/routing.py b/osinaweb/osichat/routing.py index 21d897e3..dbac54b8 100644 --- a/osinaweb/osichat/routing.py +++ b/osinaweb/osichat/routing.py @@ -2,6 +2,7 @@ from django.urls import path from .consumers import * websocket_urlpatterns = [ + path("ws/osichat/visitors/", OsitcomVisitor.as_asgi()), path("ws/osichat/", Osichat.as_asgi()), path("ws/osichat//", OsitcomChatRoom.as_asgi()), path("ws/osichat-admin///", OsitcomChatRoom.as_asgi()), diff --git a/osinaweb/osinacore/templates/chat_templates/partials/new-visitor.html b/osinaweb/osinacore/templates/chat_templates/partials/new-visitor.html new file mode 100644 index 00000000..11b1c228 --- /dev/null +++ b/osinaweb/osinacore/templates/chat_templates/partials/new-visitor.html @@ -0,0 +1,24 @@ +{% load static %} +
+
+
+ {% if not visitor.country == 'Unknown' %} + + {% else %} + + {% endif %} + + {% if visitor.is_online %} +
+ {% endif %} +
+ +
+

{{visitor.ip_address}}

+

{{visitor.visitorlog_set.all.last.url}}

+
+ +
+ +

{{visitor.total_duration}}

+
\ No newline at end of file diff --git a/osinaweb/osinacore/templates/chat_templates/partials/rooms.html b/osinaweb/osinacore/templates/chat_templates/partials/rooms.html index b46c1675..7a621c7c 100644 --- a/osinaweb/osinacore/templates/chat_templates/partials/rooms.html +++ b/osinaweb/osinacore/templates/chat_templates/partials/rooms.html @@ -28,5 +28,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/osinaweb/osinacore/templates/chat_templates/partials/visitors.html b/osinaweb/osinacore/templates/chat_templates/partials/visitors.html index 4af561b1..4e82a7be 100644 --- a/osinaweb/osinacore/templates/chat_templates/partials/visitors.html +++ b/osinaweb/osinacore/templates/chat_templates/partials/visitors.html @@ -1,30 +1,47 @@ +{% load static %} -
+
-

Active

+

Visitors today

- {% for visitor in visitors %} -
-
-
- -
+
+ {% for visitor in visitors %} +
+
+
+ {% if not visitor.country == 'Unknown' %} + + {% else %} + + {% endif %} + + {% if visitor.is_online %} +
+ {% endif %} +
-
-

{{visitor.ip_address}}

-

{{visitor.visitorlog_set.all.last.url}}

+
+

{{visitor.ip_address}}

+

+ {% if not visitor.visitorlog_set.all.last.title %} + {{ visitor.visitorlog_set.all.last.url|slice:":26" }}{% if visitor.visitorlog_set.all.last.url|length > 26 %}…{% endif %} + {% else %} + {{ visitor.visitorlog_set.all.last.title|slice:":26" }}{% if visitor.visitorlog_set.all.last.title|length > 26 %}…{% endif %} + {% endif %} +

+
+
- -
-

00:00

+

{{visitor.total_duration}}

+
+ {% endfor %}
- {% endfor %}
- \ No newline at end of file + \ No newline at end of file diff --git a/osinaweb/static/dist/output.css b/osinaweb/static/dist/output.css index ac0ba418..3e386ca7 100644 --- a/osinaweb/static/dist/output.css +++ b/osinaweb/static/dist/output.css @@ -1453,10 +1453,6 @@ video { width: 100%; } -.w-\[\] { - width: ; -} - .min-w-full { min-width: 100%; } @@ -2264,11 +2260,21 @@ video { fill: #374a7a; } +.object-contain { + -o-object-fit: contain; + object-fit: contain; +} + .object-cover { -o-object-fit: cover; object-fit: cover; } +.object-center { + -o-object-position: center; + object-position: center; +} + .p-1 { padding: 0.25rem; } @@ -3228,7 +3234,6 @@ video { .onlineDot { width: 15px; height: 15px; - background-color: #1ABC9C; /* Fallback color */ border-radius: 50%; position: relative; diff --git a/osinaweb/static/js/osichat-admin/rooms.js b/osinaweb/static/js/osichat-admin/rooms.js index 97102345..a17355ee 100644 --- a/osinaweb/static/js/osichat-admin/rooms.js +++ b/osinaweb/static/js/osichat-admin/rooms.js @@ -55,6 +55,7 @@ function initializeOsichat(){ case 'get_visitors': leftDynamicDiv.innerHTML = data.html; + setupDurationUpdater(); break; case 'new_chat_update': @@ -90,9 +91,31 @@ function initializeOsichat(){ } } - + setInterval(updateOnlineDurations, 1000); appendInnerConversationScript(leftDiv); break; + + case 'new_visitor_update': + const visitorsContainer = document.getElementById('visitors'); + const visitorDiv = visitorsContainer.querySelector(`.visitor[data-visitorid='${data.visitor_id}']`); + if (data.action === 'new_log') { + if (visitorDiv){ + visitorDiv.remove(); + } + const newVisitorDiv = document.createElement('div'); + newVisitorDiv.innerHTML = data.html; + visitorsContainer.insertAdjacentElement('afterbegin', newVisitorDiv.firstElementChild); + const visitorNotificationSound = document.getElementById('visitor-notification-sound'); + visitorNotificationSound.play(); + } else if (data.action === 'end_log') { + const newVisitorDiv = document.createElement('div'); + newVisitorDiv.innerHTML = data.html; + visitorDiv.replaceWith(newVisitorDiv.firstElementChild); + } + + + setupDurationUpdater(); + break; default: console.log('Unknown event type:', data.event_type); @@ -149,5 +172,46 @@ function switchToVisitors() { chatsTab.addEventListener('click', switchToChats); visitorsTab.addEventListener('click', switchToVisitors); +let updateInterval; +function updateOnlineDurations() { + document.querySelectorAll('.visitor').forEach(visitorDiv => { + const onlineIndicator = visitorDiv.querySelector('.online'); + if (onlineIndicator) { + const durationElem = visitorDiv.querySelector('.duration'); + const currentText = durationElem.textContent; + const parts = currentText.split(':').map(Number); + + let hours = 0, minutes = 0, seconds = 0; + + if (parts.length === 3) { + [hours, minutes, seconds] = parts; + } else if (parts.length === 2) { + [minutes, seconds] = parts; + } + + const totalSeconds = hours * 3600 + minutes * 60 + seconds; + const updatedTotalSeconds = totalSeconds + 1; + const updatedHours = Math.floor(updatedTotalSeconds / 3600); + const updatedMinutes = Math.floor((updatedTotalSeconds % 3600) / 60); + const updatedSeconds = updatedTotalSeconds % 60; + + const formattedDuration = + (updatedHours > 0 ? `${String(updatedHours).padStart(2, '0')}:` : '') + + `${String(updatedMinutes).padStart(2, '0')}:${String(updatedSeconds).padStart(2, '0')}`; + + durationElem.textContent = formattedDuration; + } + }); +} + +function setupDurationUpdater() { + // Clear any existing interval to avoid multiple intervals + if (updateInterval) { + clearInterval(updateInterval); + } + // Set up a new interval + updateInterval = setInterval(updateOnlineDurations, 1000); +} + initializeOsichat(); \ No newline at end of file diff --git a/osinaweb/static/js/osichat-admin/update-visitors-duration.js b/osinaweb/static/js/osichat-admin/update-visitors-duration.js new file mode 100644 index 00000000..d12c18ef --- /dev/null +++ b/osinaweb/static/js/osichat-admin/update-visitors-duration.js @@ -0,0 +1,27 @@ +function updateOnlineDurations() { + document.querySelectorAll('.visitor').forEach(visitorDiv => { + if (visitorDiv.querySelector('.online')) { + const durationElem = visitorDiv.querySelector('.duration'); + const currentText = durationElem.textContent; + const [hours, minutes, seconds] = currentText.split(':').map(Number); + const totalSeconds = hours * 3600 + minutes * 60 + seconds; + + // Increment the duration by 1 second + const updatedTotalSeconds = totalSeconds + 1; + const updatedHours = Math.floor(updatedTotalSeconds / 3600); + const updatedMinutes = Math.floor((updatedTotalSeconds % 3600) / 60); + const updatedSeconds = updatedTotalSeconds % 60; + + // Format the updated duration + const formattedDuration = + (updatedHours > 0 ? `${String(updatedHours).padStart(2, '0') + ':' : ''}` + + `${String(updatedMinutes).padStart(2, '0')}:${String(updatedSeconds).padStart(2, '0')}`); + + // Update the duration element + durationElem.textContent = formattedDuration; + } + }); +} + +// Call this function at intervals to update durations +setInterval(updateOnlineDurations, 1000); // Update every second diff --git a/osinaweb/static/js/osichat/visitors.js b/osinaweb/static/js/osichat/visitors.js index c5f36492..5f16f4e4 100644 --- a/osinaweb/static/js/osichat/visitors.js +++ b/osinaweb/static/js/osichat/visitors.js @@ -3,6 +3,7 @@ const protocol = window.location.protocol === "https:" ? "https" : "http"; const osinaDomain = "osina.ositcom.com"; const ositcomDomain = "ositcom.com"; + async function fetchClientData() { let clientData = { client_ip: 'Unknown', client_country: 'Unknown' }; while (clientData.client_ip === 'Unknown') { @@ -39,23 +40,69 @@ async function fetchVisitorsSession() { return session_id; } + + +const getBrowserInfo = () => { + const userAgent = navigator.userAgent; + let browserName = "Unknown"; + let osName = "Unknown"; + + if (userAgent.indexOf("Firefox") > -1) { + browserName = "Firefox"; + } else if (userAgent.indexOf("SamsungBrowser") > -1) { + browserName = "Samsung Internet"; + } else if (userAgent.indexOf("Opera") > -1 || userAgent.indexOf("OPR") > -1) { + browserName = "Opera"; + } else if (userAgent.indexOf("Trident") > -1) { + browserName = "Internet Explorer"; + } else if (userAgent.indexOf("Edge") > -1) { + browserName = "Edge"; + } else if (userAgent.indexOf("Chrome") > -1) { + browserName = "Chrome"; + } else if (userAgent.indexOf("Safari") > -1) { + browserName = "Safari"; + } + + if (userAgent.indexOf("Win") > -1) { + osName = "Windows"; + } else if (userAgent.indexOf("Mac") > -1) { + osName = "MacOS"; + } else if (userAgent.indexOf("X11") > -1) { + osName = "UNIX"; + } else if (userAgent.indexOf("Linux") > -1) { + osName = "Linux"; + } else if (userAgent.indexOf("Android") > -1) { + osName = "Android"; + } else if (userAgent.indexOf("like Mac") > -1) { + osName = "iOS"; + } + + return { browserName, osName }; +}; + + async function initializeVisitorsWebSocket() { const referrer = document.referrer; const clientData = await fetchClientData(); const session_id = await fetchVisitorsSession(); + const browserInfo = getBrowserInfo(); const visitorsSocketUrl = `${wsScheme}://${osinaDomain}/ws/osichat/visitors/`; const visitorsSocket = new WebSocket(visitorsSocketUrl); visitorsSocket.onopen = () => { console.log('WebSocket connection to visitors established'); + console.log('alooo'+window.document.title) const event_message = { 'event_type': 'visitor_ping', 'referrer': referrer, 'url': window.location.href, + 'title': window.document.title, 'client_ip': clientData.client_ip, 'client_country': clientData.client_country, + 'browser_name': browserInfo.browserName, + 'os_name': browserInfo.osName, 'session_id': session_id }; visitorsSocket.send(JSON.stringify(event_message)); diff --git a/osinaweb/static/notifications/new-visitor.mp3 b/osinaweb/static/notifications/new-visitor.mp3 new file mode 100644 index 00000000..8c81acf0 Binary files /dev/null and b/osinaweb/static/notifications/new-visitor.mp3 differ