diff --git a/sylk/applications/webrtcgateway/datatypes.py b/sylk/applications/webrtcgateway/datatypes.py index f3e1e32..f4121d9 100644 --- a/sylk/applications/webrtcgateway/datatypes.py +++ b/sylk/applications/webrtcgateway/datatypes.py @@ -1,113 +1,120 @@ import base64 import hashlib import json import os +from datetime import datetime, timedelta import urllib.parse from sipsimple.util import ISOTimestamp from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.streams.msrp.chat import CPIMPayload, ChatIdentity, CPIMHeader, CPIMNamespace from sylk.web import server from sylk.configuration.datatypes import URL +from .configuration import GeneralConfig from .models import sylkrtc class FileTransferData(object): - def __init__(self, filename, filesize, filetype, transfer_id, sender, receiver, content=None): + def __init__(self, filename, filesize, filetype, transfer_id, sender, receiver, content=None, url=None): self.content = content self.filename = filename self.filesize = filesize self.filetype = filetype self.transfer_id = transfer_id self.sender = sylkrtc.SIPIdentity(uri=sender, display_name='') self.receiver = sylkrtc.SIPIdentity(uri=receiver, display_name='') self.hashed_sender = sender self.hashed_receiver = receiver self.prefix = self.hashed_sender[:1] self.path = self._get_initial_path() self.timestamp = str(ISOTimestamp.now()) - self.url = self._set_url() + self.until = self._set_until() + self.url = self._set_url() if not url else url def _encode_and_hash_uri(self, uri): return base64.urlsafe_b64encode(hashlib.md5(uri.encode('utf-8')).digest()).rstrip(b'=\n').decode('utf-8') def _get_initial_path(self): settings = SIPSimpleSettings() return os.path.join(settings.file_transfer.directory.normalized, self.prefix, self.hashed_sender, self.hashed_receiver, self.transfer_id) def update_path_for_receiver(self): settings = SIPSimpleSettings() self.prefix = self.hashed_receiver[:1] self.path = os.path.join(settings.file_transfer.directory.normalized, self.prefix, self.hashed_receiver, self.hashed_sender, self.transfer_id) self.url = self._set_url() + def _set_until(self): + remove_after_days = GeneralConfig.filetransfer_expire_days + return str(ISOTimestamp(datetime.now() + timedelta(days=remove_after_days))) + def _set_url(self): settings = SIPSimpleSettings() stripped_path = os.path.relpath(self.path, f'{settings.file_transfer.directory.normalized}/{self.prefix}') file_path = urllib.parse.quote(f'webrtcgateway/filetransfer/{stripped_path}/{self.filename}') return str(URL(f'{server.url}/{file_path}')) @property def full_path(self): return os.path.join(self.path, self.filename) @property def hashed_sender(self): return self._hashed_sender @hashed_sender.setter def hashed_sender(self, value): self._hashed_sender = self._encode_and_hash_uri(value) @property def hashed_receiver(self): return self._hashed_receiver @hashed_receiver.setter def hashed_receiver(self, value): self._hashed_receiver = self._encode_and_hash_uri(value) @property def message_payload(self): return f'File transfer available at {self.url} ({self.formatted_file_size})' def cpim_message_payload(self, metadata): return self.build_cpim_payload(self.sender.uri, self.receiver.uri, self.transfer_id, json.dumps(sylkrtc.FileTransferMessage(**metadata.__data__).__data__), content_type='application/sylk-file-transfer') @property def formatted_file_size(self): return self.format_file_size(self.filesize) @staticmethod def build_cpim_payload(account, uri, message_id, content, content_type='text/plain'): ns = CPIMNamespace('urn:ietf:params:imdn', 'imdn') additional_headers = [CPIMHeader('Message-ID', ns, message_id)] additional_headers.append(CPIMHeader('Disposition-Notification', ns, 'positive-delivery, display')) payload = CPIMPayload(content, content_type, charset='utf-8', sender=ChatIdentity(account, None), recipients=[ChatIdentity(uri, None)], timestamp=str(ISOTimestamp.now()), additional_headers=additional_headers) payload, content_type = payload.encode() return payload @staticmethod def format_file_size(size): infinite = float('infinity') boundaries = [( 1024, '%d bytes', 1), ( 10*1024, '%.2f KB', 1024.0), ( 1024*1024, '%.1f KB', 1024.0), ( 10*1024*1024, '%.2f MB', 1024*1024.0), (1024*1024*1024, '%.1f MB', 1024*1024.0), (10*1024*1024*1024, '%.2f GB', 1024*1024*1024.0), ( infinite, '%.1f GB', 1024*1024*1024.0)] for boundary, format, divisor in boundaries: if size < boundary: return format % (size/divisor,) else: return "%d bytes" % size diff --git a/sylk/applications/webrtcgateway/models/sylkrtc.py b/sylk/applications/webrtcgateway/models/sylkrtc.py index 9306b93..8962d29 100644 --- a/sylk/applications/webrtcgateway/models/sylkrtc.py +++ b/sylk/applications/webrtcgateway/models/sylkrtc.py @@ -1,648 +1,650 @@ from application.python import subclasses from .jsonobjects import BooleanProperty, IntegerProperty, StringProperty, ArrayProperty, ObjectProperty, FixedValueProperty, LimitedChoiceProperty, AbstractObjectProperty, AbstractProperty from .jsonobjects import JSONObject, JSONArray, StringArray, CompositeValidator from .validators import AORValidator, DisplayNameValidator, LengthValidator, UniqueItemsValidator from sipsimple.util import ISOTimestamp # Base models (these are abstract and should not be used directly) class SylkRTCRequestBase(JSONObject): transaction = StringProperty() class SylkRTCResponseBase(JSONObject): transaction = StringProperty() class AccountRequestBase(SylkRTCRequestBase): account = StringProperty(validator=AORValidator()) class SessionRequestBase(SylkRTCRequestBase): session = StringProperty() class VideoroomRequestBase(SylkRTCRequestBase): session = StringProperty() class AccountEventBase(JSONObject): sylkrtc = FixedValueProperty('account-event') account = StringProperty(validator=AORValidator()) class SessionEventBase(JSONObject): sylkrtc = FixedValueProperty('session-event') session = StringProperty() class VideoroomEventBase(JSONObject): sylkrtc = FixedValueProperty('videoroom-event') session = StringProperty() class AccountRegistrationStateEvent(AccountEventBase): event = FixedValueProperty('registration-state') class SessionStateEvent(SessionEventBase): event = FixedValueProperty('state') class VideoroomSessionStateEvent(VideoroomEventBase): event = FixedValueProperty('session-state') # Miscellaneous models class Header(JSONObject): name = StringProperty() value = StringProperty() class Headers(JSONArray): item_type = Header class SIPIdentity(JSONObject): uri = StringProperty(validator=AORValidator()) display_name = StringProperty(optional=True, validator=DisplayNameValidator()) class ICECandidate(JSONObject): candidate = StringProperty() sdpMLineIndex = IntegerProperty() sdpMid = StringProperty() class ICECandidates(JSONArray): item_type = ICECandidate class AORList(StringArray): list_validator = UniqueItemsValidator() item_validator = AORValidator() class VideoroomPublisher(JSONObject): id = StringProperty() uri = StringProperty(validator=AORValidator()) display_name = StringProperty(optional=True) class VideoroomPublishers(JSONArray): item_type = VideoroomPublisher class VideoroomActiveParticipants(StringArray): list_validator = CompositeValidator(UniqueItemsValidator(), LengthValidator(maximum=2)) class VideoroomSessionOptions(JSONObject): audio = BooleanProperty(optional=True) video = BooleanProperty(optional=True) bitrate = IntegerProperty(optional=True) class VideoroomRaisedHands(StringArray): list_validator = UniqueItemsValidator() class SharedFile(JSONObject): filename = StringProperty() filesize = IntegerProperty() uploader = ObjectProperty(SIPIdentity) # type: SIPIdentity session = StringProperty() class SharedFiles(JSONArray): item_type = SharedFile class TransferredFile(JSONObject): filename = StringProperty() filesize = IntegerProperty() sender = ObjectProperty(SIPIdentity) # type: SIPIdentity receiver = ObjectProperty(SIPIdentity) transfer_id = StringProperty() prefix = StringProperty() path = StringProperty() timestamp = StringProperty() + until = StringProperty(optional=True) url = StringProperty(optional=True) filetype = StringProperty(optional=True) hash = StringProperty(optional=True) class FileTransferMessage(JSONObject): filename = StringProperty() filesize = IntegerProperty() sender = ObjectProperty(SIPIdentity) # type: SIPIdentity receiver = ObjectProperty(SIPIdentity) transfer_id = StringProperty() timestamp = StringProperty() + until = StringProperty(optional=True) url = StringProperty(optional=True) filetype = StringProperty(optional=True) hash = StringProperty(optional=True) class DispositionNotifications(StringArray): list_validator = UniqueItemsValidator() class Message(JSONObject): contact = StringProperty(validator=AORValidator()) timestamp = StringProperty() disposition = ArrayProperty(DispositionNotifications, optional=True) message_id = StringProperty() content_type = StringProperty() content = StringProperty() direction = StringProperty(optional=True) state = LimitedChoiceProperty(['delivered', 'failed', 'displayed', 'forbidden', 'error', 'accepted', 'pending', 'received'], optional=True) def __init__(self, **kw): if 'msg_timestamp' in kw: kw['timestamp'] = str(ISOTimestamp(kw['msg_timestamp'])) del kw['msg_timestamp'] super(Message, self).__init__(**kw) class ContactMessages(JSONArray): item_type = Message class MessageHistoryData(JSONObject): account = StringProperty(validator=AORValidator()) messages = ArrayProperty(ContactMessages) class AccountMessageRemoveEventData(JSONObject): contact = StringProperty() message_id = StringProperty() class AccountMarkConversationReadEventData(JSONObject): contact = StringProperty() class AccountConversationRemoveEventData(JSONObject): contact = StringProperty() class AccountDispositionNotificationEventData(JSONObject): message_id = StringProperty() state = LimitedChoiceProperty(['accepted', 'delivered', 'displayed', 'failed', 'processed', 'stored', 'forbidden', 'error']) message_timstamp = StringProperty() code = IntegerProperty() reason = StringProperty() class IncomingHeaderPrefixes(StringArray): list_validator = UniqueItemsValidator() # Response models class AckResponse(SylkRTCResponseBase): sylkrtc = FixedValueProperty('ack') class ErrorResponse(SylkRTCResponseBase): sylkrtc = FixedValueProperty('error') error = StringProperty() # Connection events class ReadyEvent(JSONObject): sylkrtc = FixedValueProperty('ready-event') class LookupPublicKeyEvent(JSONObject): sylkrtc = FixedValueProperty('lookup-public-key-event') uri = StringProperty(validator=AORValidator()) public_key = StringProperty(optional=True) # Account events class AccountIncomingSessionEvent(AccountEventBase): event = FixedValueProperty('incoming-session') session = StringProperty() originator = ObjectProperty(SIPIdentity) # type: SIPIdentity sdp = StringProperty() call_id = StringProperty() headers = AbstractProperty(optional=True) class AccountMissedSessionEvent(AccountEventBase): event = FixedValueProperty('missed-session') originator = ObjectProperty(SIPIdentity) # type: SIPIdentity class AccountConferenceInviteEvent(AccountEventBase): event = FixedValueProperty('conference-invite') room = StringProperty(validator=AORValidator()) session_id = StringProperty() originator = ObjectProperty(SIPIdentity) # type: SIPIdentity class AccountMessageEvent(AccountEventBase): event = FixedValueProperty('message') sender = ObjectProperty(SIPIdentity) # type: SIPIdentity timestamp = StringProperty() disposition_notification = ArrayProperty(DispositionNotifications, optional=True) message_id = StringProperty() content_type = StringProperty() content = StringProperty() direction = StringProperty(optional=True) class AccountDispositionNotificationEvent(AccountEventBase): event = FixedValueProperty('disposition-notification') message_id = StringProperty() message_timestamp = StringProperty() state = LimitedChoiceProperty(['accepted', 'delivered', 'displayed', 'failed', 'processed', 'stored', 'forbidden', 'error']) code = IntegerProperty() reason = StringProperty() class AccountSyncConversationsEvent(AccountEventBase): event = FixedValueProperty('sync-conversations') messages = ArrayProperty(ContactMessages) class AccountSyncEvent(AccountEventBase): event = FixedValueProperty('sync') type = StringProperty() action = StringProperty() content = AbstractObjectProperty() class AccountRegisteringEvent(AccountRegistrationStateEvent): state = FixedValueProperty('registering') class AccountRegisteredEvent(AccountRegistrationStateEvent): state = FixedValueProperty('registered') class AccountRegistrationFailedEvent(AccountRegistrationStateEvent): state = FixedValueProperty('failed') reason = StringProperty(optional=True) # Session events class SessionProgressEvent(SessionStateEvent): state = FixedValueProperty('progress') class SessionEarlyMediaEvent(SessionStateEvent): state = FixedValueProperty('early-media') sdp = StringProperty(optional=True) call_id = StringProperty(optional=True) class SessionAcceptedEvent(SessionStateEvent): state = FixedValueProperty('accepted') sdp = StringProperty(optional=True) # missing for incoming sessions call_id = StringProperty(optional=True) headers = AbstractProperty(optional=True) class SessionEstablishedEvent(SessionStateEvent): state = FixedValueProperty('established') class SessionTerminatedEvent(SessionStateEvent): state = FixedValueProperty('terminated') reason = StringProperty(optional=True) class SessionMessageEvent(SessionEventBase): event = FixedValueProperty('message') sender = ObjectProperty(SIPIdentity) # type: SIPIdentity timestamp = StringProperty() disposition_notification = ArrayProperty(DispositionNotifications, optional=True) message_id = StringProperty() content_type = StringProperty() content = StringProperty() direction = StringProperty(optional=True) class SessionMessageDispositionNotificationEvent(SessionEventBase): event = FixedValueProperty('disposition-notification') message_id = StringProperty() message_timestamp = StringProperty() state = LimitedChoiceProperty(['accepted', 'delivered', 'displayed', 'failed', 'processed', 'stored', 'forbidden', 'error']) code = IntegerProperty() reason = StringProperty() # Video room events class VideoroomConfigureEvent(VideoroomEventBase): event = FixedValueProperty('configure') originator = StringProperty() active_participants = ArrayProperty(VideoroomActiveParticipants) # type: VideoroomActiveParticipants class VideoroomSessionProgressEvent(VideoroomSessionStateEvent): state = FixedValueProperty('progress') class VideoroomSessionAcceptedEvent(VideoroomSessionStateEvent): state = FixedValueProperty('accepted') sdp = StringProperty() video = BooleanProperty(optional=True, default=True) audio = BooleanProperty(optional=True, default=True) class VideoroomSessionEstablishedEvent(VideoroomSessionStateEvent): state = FixedValueProperty('established') class VideoroomSessionTerminatedEvent(VideoroomSessionStateEvent): state = FixedValueProperty('terminated') reason = StringProperty(optional=True) class VideoroomFeedAttachedEvent(VideoroomEventBase): event = FixedValueProperty('feed-attached') feed = StringProperty() sdp = StringProperty() class VideoroomFeedEstablishedEvent(VideoroomEventBase): event = FixedValueProperty('feed-established') feed = StringProperty() class VideoroomInitialPublishersEvent(VideoroomEventBase): event = FixedValueProperty('initial-publishers') publishers = ArrayProperty(VideoroomPublishers) # type: VideoroomPublishers class VideoroomPublishersJoinedEvent(VideoroomEventBase): event = FixedValueProperty('publishers-joined') publishers = ArrayProperty(VideoroomPublishers) # type: VideoroomPublishers class VideoroomPublishersLeftEvent(VideoroomEventBase): event = FixedValueProperty('publishers-left') publishers = ArrayProperty(StringArray) # type: StringArray class VideoroomFileSharingEvent(VideoroomEventBase): event = FixedValueProperty('file-sharing') files = ArrayProperty(SharedFiles) # type: SharedFiles class VideoroomMessageEvent(VideoroomEventBase): event = FixedValueProperty('message') type = LimitedChoiceProperty(['normal', 'status']) content = StringProperty() content_type = StringProperty() sender = ObjectProperty(SIPIdentity) # type: SIPIdentity timestamp = StringProperty() class VideoroomComposingIndicationEvent(VideoroomEventBase): event = FixedValueProperty('composing-indication') state = StringProperty() refresh = IntegerProperty() content_type = StringProperty() sender = ObjectProperty(SIPIdentity) # type: SIPIdentity class VideoroomMessageDeliveryEvent(VideoroomEventBase): event = FixedValueProperty('message-delivery') message_id = StringProperty() delivered = BooleanProperty() code = IntegerProperty() reason = StringProperty() class VideoroomMuteAudioEvent(VideoroomEventBase): event = FixedValueProperty('mute-audio') originator = StringProperty() class VideoroomRaisedHandsEvent(VideoroomEventBase): event = FixedValueProperty('raised-hands') raised_hands = ArrayProperty(VideoroomRaisedHands) # Ping request model, can be used to check connectivity from client class PingRequest(SylkRTCRequestBase): sylkrtc = FixedValueProperty('ping') # Lookup Public key model class LookupPublicKeyRequest(SylkRTCRequestBase): sylkrtc = FixedValueProperty('lookup-public-key') uri = StringProperty(validator=AORValidator()) # Account request models class AccountAddRequest(AccountRequestBase): sylkrtc = FixedValueProperty('account-add') password = StringProperty(validator=LengthValidator(minimum=1, maximum=9999)) display_name = StringProperty(optional=True) user_agent = StringProperty(optional=True) incoming_header_prefixes = ArrayProperty(IncomingHeaderPrefixes, optional=True) class AccountRemoveRequest(AccountRequestBase): sylkrtc = FixedValueProperty('account-remove') class AccountRegisterRequest(AccountRequestBase): sylkrtc = FixedValueProperty('account-register') class AccountUnregisterRequest(AccountRequestBase): sylkrtc = FixedValueProperty('account-unregister') class AccountDeviceTokenRequest(AccountRequestBase): sylkrtc = FixedValueProperty('account-devicetoken') token = StringProperty() platform = StringProperty() device = StringProperty() silent = BooleanProperty(default=False) app = StringProperty() class AccountMessageRequest(AccountRequestBase): sylkrtc = FixedValueProperty('account-message') uri = StringProperty(validator=AORValidator()) message_id = StringProperty() content = StringProperty() content_type = StringProperty() timestamp = StringProperty() server_generated = BooleanProperty(optional=True) class AccountDispositionNotificationRequest(AccountRequestBase): sylkrtc = FixedValueProperty('account-disposition-notification') uri = StringProperty(validator=AORValidator()) message_id = StringProperty() state = LimitedChoiceProperty(['delivered', 'failed', 'displayed', 'forbidden', 'error']) timestamp = StringProperty() class AccountSyncConversationsRequest(AccountRequestBase): sylkrtc = FixedValueProperty('account-sync-conversations') message_id = StringProperty(optional=True) class AccountMarkConversationReadRequest(AccountRequestBase): sylkrtc = FixedValueProperty('account-mark-conversation-read') contact = StringProperty(validator=AORValidator()) class AccountMessageRemoveRequest(AccountRequestBase): sylkrtc = FixedValueProperty('account-remove-message') message_id = StringProperty() contact = StringProperty(validator=AORValidator()) class AccountConversationRemoveRequest(AccountRequestBase): sylkrtc = FixedValueProperty('account-remove-conversation') contact = StringProperty(validator=AORValidator()) # Session request models class SessionCreateRequest(SessionRequestBase): sylkrtc = FixedValueProperty('session-create') account = StringProperty(validator=AORValidator()) uri = StringProperty(validator=AORValidator()) sdp = StringProperty() headers = ArrayProperty(Headers, optional=True) class SessionAnswerRequest(SessionRequestBase): sylkrtc = FixedValueProperty('session-answer') sdp = StringProperty() headers = ArrayProperty(Headers, optional=True) class SessionTrickleRequest(SessionRequestBase): sylkrtc = FixedValueProperty('session-trickle') candidates = ArrayProperty(ICECandidates) # type: ICECandidates class SessionTerminateRequest(SessionRequestBase): sylkrtc = FixedValueProperty('session-terminate') class SessionMessageRequest(SessionRequestBase): sylkrtc = FixedValueProperty('session-message') message_id = StringProperty() content = StringProperty() content_type = StringProperty() timestamp = StringProperty() # Videoroom request models class VideoroomJoinRequest(VideoroomRequestBase): sylkrtc = FixedValueProperty('videoroom-join') account = StringProperty(validator=AORValidator()) uri = StringProperty(validator=AORValidator()) sdp = StringProperty() audio = BooleanProperty(optional=True, default=True) video = BooleanProperty(optional=True, default=True) class VideoroomLeaveRequest(VideoroomRequestBase): sylkrtc = FixedValueProperty('videoroom-leave') class VideoroomConfigureRequest(VideoroomRequestBase): sylkrtc = FixedValueProperty('videoroom-configure') active_participants = ArrayProperty(VideoroomActiveParticipants) # type: VideoroomActiveParticipants class VideoroomFeedAttachRequest(VideoroomRequestBase): sylkrtc = FixedValueProperty('videoroom-feed-attach') publisher = StringProperty() feed = StringProperty() class VideoroomFeedAnswerRequest(VideoroomRequestBase): sylkrtc = FixedValueProperty('videoroom-feed-answer') feed = StringProperty() sdp = StringProperty() class VideoroomFeedDetachRequest(VideoroomRequestBase): sylkrtc = FixedValueProperty('videoroom-feed-detach') feed = StringProperty() class VideoroomInviteRequest(VideoroomRequestBase): sylkrtc = FixedValueProperty('videoroom-invite') participants = ArrayProperty(AORList) # type: AORList class VideoroomSessionTrickleRequest(VideoroomRequestBase): sylkrtc = FixedValueProperty('videoroom-session-trickle') candidates = ArrayProperty(ICECandidates) # type: ICECandidates class VideoroomSessionUpdateRequest(VideoroomRequestBase): sylkrtc = FixedValueProperty('videoroom-session-update') options = ObjectProperty(VideoroomSessionOptions) # type: VideoroomSessionOptions class VideoroomMessageRequest(VideoroomRequestBase): sylkrtc = FixedValueProperty('videoroom-message') message_id = StringProperty() content = StringProperty() content_type = StringProperty() class VideoroomComposingIndicationRequest(VideoroomRequestBase): sylkrtc = FixedValueProperty('videoroom-composing-indication') state = LimitedChoiceProperty(['active', 'idle']) refresh = IntegerProperty(optional=True) class VideoroomMuteAudioParticipantsRequest(VideoroomRequestBase): sylkrtc = FixedValueProperty('videoroom-mute-audio-participants') class VideoroomToggleHandRequest(VideoroomRequestBase): sylkrtc = FixedValueProperty('videoroom-toggle-hand') session_id = StringProperty(optional=True) # SylkRTC request to model mapping class ProtocolError(Exception): pass class SylkRTCRequest(object): __classmap__ = {cls.sylkrtc.value: cls for cls in subclasses(SylkRTCRequestBase) if hasattr(cls, 'sylkrtc')} @classmethod def from_message(cls, message): try: request_type = message['sylkrtc'] except KeyError: raise ProtocolError('could not get WebSocket message type') try: request_class = cls.__classmap__[request_type] except KeyError: raise ProtocolError('unknown WebSocket request: %s' % request_type) return request_class(**message)