Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
V
vmj-qt
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Kulya
vmj-qt
Commits
b3a8f270
Commit
b3a8f270
authored
Jun 14, 2021
by
Adrian Georgescu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Refactored TLS settings based on the latest middleware specifications
parent
4e736c9a
Changes
6
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
630 additions
and
473 deletions
+630
-473
account.py
blink/configuration/account.py
+3
-6
settings.py
blink/configuration/settings.py
+2
-0
preferences.py
blink/preferences.py
+34
-28
sessions.py
blink/sessions.py
+2
-2
control
debian/control
+1
-1
preferences.ui
resources/preferences.ui
+588
-436
No files found.
blink/configuration/account.py
View file @
b3a8f270
...
@@ -3,7 +3,7 @@
...
@@ -3,7 +3,7 @@
__all__
=
[
'AccountExtension'
,
'BonjourAccountExtension'
]
__all__
=
[
'AccountExtension'
,
'BonjourAccountExtension'
]
from
sipsimple.account
import
BonjourMSRPSettings
,
MessageSummarySettings
,
MSRPSettings
,
PresenceSettings
,
RTPSettings
,
SIPSettings
,
TLSSettings
,
XCAPSettings
from
sipsimple.account
import
BonjourMSRPSettings
,
MessageSummarySettings
,
MSRPSettings
,
PresenceSettings
,
RTPSettings
,
SIPSettings
,
XCAPSettings
from
sipsimple.configuration
import
Setting
,
SettingsGroup
,
SettingsObjectExtension
,
RuntimeSetting
from
sipsimple.configuration
import
Setting
,
SettingsGroup
,
SettingsObjectExtension
,
RuntimeSetting
from
sipsimple.configuration.datatypes
import
AudioCodecList
,
Hostname
,
MSRPConnectionModel
,
MSRPTransport
,
NonNegativeInteger
,
SIPTransportList
,
VideoCodecList
from
sipsimple.configuration.datatypes
import
AudioCodecList
,
Hostname
,
MSRPConnectionModel
,
MSRPTransport
,
NonNegativeInteger
,
SIPTransportList
,
VideoCodecList
from
sipsimple.util
import
user_info
from
sipsimple.util
import
user_info
...
@@ -18,6 +18,7 @@ class BonjourMSRPSettingsExtension(BonjourMSRPSettings):
...
@@ -18,6 +18,7 @@ class BonjourMSRPSettingsExtension(BonjourMSRPSettings):
class
BonjourSIPSettings
(
SettingsGroup
):
class
BonjourSIPSettings
(
SettingsGroup
):
transport_order
=
Setting
(
type
=
SIPTransportList
,
default
=
SIPTransportList
([
'tcp'
,
'udp'
,
'tls'
]))
transport_order
=
Setting
(
type
=
SIPTransportList
,
default
=
SIPTransportList
([
'tcp'
,
'udp'
,
'tls'
]))
tls_name
=
Setting
(
type
=
str
,
default
=
'Blink'
)
class
MessageSummarySettingsExtension
(
MessageSummarySettings
):
class
MessageSummarySettingsExtension
(
MessageSummarySettings
):
...
@@ -50,6 +51,7 @@ class SIPSettingsExtension(SIPSettings):
...
@@ -50,6 +51,7 @@ class SIPSettingsExtension(SIPSettings):
register_interval
=
Setting
(
type
=
NonNegativeInteger
,
default
=
600
)
register_interval
=
Setting
(
type
=
NonNegativeInteger
,
default
=
600
)
subscribe_interval
=
Setting
(
type
=
NonNegativeInteger
,
default
=
600
)
subscribe_interval
=
Setting
(
type
=
NonNegativeInteger
,
default
=
600
)
publish_interval
=
Setting
(
type
=
NonNegativeInteger
,
default
=
600
)
publish_interval
=
Setting
(
type
=
NonNegativeInteger
,
default
=
600
)
tls_name
=
Setting
(
type
=
str
,
default
=
None
,
nillable
=
True
)
class
ServerSettings
(
SettingsGroup
):
class
ServerSettings
(
SettingsGroup
):
...
@@ -61,9 +63,6 @@ class SoundSettings(SettingsGroup):
...
@@ -61,9 +63,6 @@ class SoundSettings(SettingsGroup):
inbound_ringtone
=
Setting
(
type
=
SoundFile
,
default
=
None
,
nillable
=
True
)
inbound_ringtone
=
Setting
(
type
=
SoundFile
,
default
=
None
,
nillable
=
True
)
class
TLSSettingsExtension
(
TLSSettings
):
certificate
=
Setting
(
type
=
ApplicationDataPath
,
default
=
ApplicationDataPath
(
Resources
.
get
(
'tls/default.crt'
)),
nillable
=
True
)
class
XCAPSettingsExtension
(
XCAPSettings
):
class
XCAPSettingsExtension
(
XCAPSettings
):
enabled
=
Setting
(
type
=
bool
,
default
=
True
)
enabled
=
Setting
(
type
=
bool
,
default
=
True
)
...
@@ -80,7 +79,6 @@ class AccountExtension(SettingsObjectExtension):
...
@@ -80,7 +79,6 @@ class AccountExtension(SettingsObjectExtension):
server
=
ServerSettings
server
=
ServerSettings
sip
=
SIPSettingsExtension
sip
=
SIPSettingsExtension
sounds
=
SoundSettings
sounds
=
SoundSettings
tls
=
TLSSettingsExtension
xcap
=
XCAPSettingsExtension
xcap
=
XCAPSettingsExtension
...
@@ -90,6 +88,5 @@ class BonjourAccountExtension(SettingsObjectExtension):
...
@@ -90,6 +88,5 @@ class BonjourAccountExtension(SettingsObjectExtension):
rtp
=
RTPSettingsExtension
rtp
=
RTPSettingsExtension
sip
=
BonjourSIPSettings
sip
=
BonjourSIPSettings
sounds
=
SoundSettings
sounds
=
SoundSettings
tls
=
TLSSettingsExtension
blink/configuration/settings.py
View file @
b3a8f270
...
@@ -70,6 +70,8 @@ class SoundSettings(SettingsGroup):
...
@@ -70,6 +70,8 @@ class SoundSettings(SettingsGroup):
class
TLSSettingsExtension
(
TLSSettings
):
class
TLSSettingsExtension
(
TLSSettings
):
ca_list
=
Setting
(
type
=
ApplicationDataPath
,
default
=
ApplicationDataPath
(
Resources
.
get
(
'tls/ca.crt'
)),
nillable
=
True
)
ca_list
=
Setting
(
type
=
ApplicationDataPath
,
default
=
ApplicationDataPath
(
Resources
.
get
(
'tls/ca.crt'
)),
nillable
=
True
)
certificate
=
Setting
(
type
=
ApplicationDataPath
,
default
=
ApplicationDataPath
(
Resources
.
get
(
'tls/default.crt'
)),
nillable
=
True
)
verify_server
=
Setting
(
type
=
bool
,
default
=
True
)
class
SIPSimpleSettingsExtension
(
SettingsObjectExtension
):
class
SIPSimpleSettingsExtension
(
SettingsObjectExtension
):
...
...
blink/preferences.py
View file @
b3a8f270
...
@@ -251,9 +251,7 @@ class PreferencesWindow(base_class, ui_class, metaclass=QSingleton):
...
@@ -251,9 +251,7 @@ class PreferencesWindow(base_class, ui_class, metaclass=QSingleton):
self
.
reregister_button
.
clicked
.
connect
(
self
.
_SH_ReregisterButtonClicked
)
self
.
reregister_button
.
clicked
.
connect
(
self
.
_SH_ReregisterButtonClicked
)
self
.
idd_prefix_button
.
activated
[
str
]
.
connect
(
self
.
_SH_IDDPrefixButtonActivated
)
self
.
idd_prefix_button
.
activated
[
str
]
.
connect
(
self
.
_SH_IDDPrefixButtonActivated
)
self
.
prefix_button
.
activated
[
str
]
.
connect
(
self
.
_SH_PrefixButtonActivated
)
self
.
prefix_button
.
activated
[
str
]
.
connect
(
self
.
_SH_PrefixButtonActivated
)
self
.
account_tls_cert_file_editor
.
locationCleared
.
connect
(
self
.
_SH_AccountTLSCertFileEditorLocationCleared
)
self
.
account_tls_name_editor
.
editingFinished
.
connect
(
self
.
_SH_TLSPeerNameEditorEditingFinished
)
self
.
account_tls_cert_file_browse_button
.
clicked
.
connect
(
self
.
_SH_AccountTLSCertFileBrowseButtonClicked
)
self
.
account_tls_verify_server_button
.
clicked
.
connect
(
self
.
_SH_AccountTLSVerifyServerButtonClicked
)
# Audio devices
# Audio devices
self
.
audio_alert_device_button
.
activated
[
int
]
.
connect
(
self
.
_SH_AudioAlertDeviceButtonActivated
)
self
.
audio_alert_device_button
.
activated
[
int
]
.
connect
(
self
.
_SH_AudioAlertDeviceButtonActivated
)
...
@@ -331,6 +329,9 @@ class PreferencesWindow(base_class, ui_class, metaclass=QSingleton):
...
@@ -331,6 +329,9 @@ class PreferencesWindow(base_class, ui_class, metaclass=QSingleton):
# TLS settings
# TLS settings
self
.
tls_ca_file_editor
.
locationCleared
.
connect
(
self
.
_SH_TLSCAFileEditorLocationCleared
)
self
.
tls_ca_file_editor
.
locationCleared
.
connect
(
self
.
_SH_TLSCAFileEditorLocationCleared
)
self
.
tls_ca_file_browse_button
.
clicked
.
connect
(
self
.
_SH_TLSCAFileBrowseButtonClicked
)
self
.
tls_ca_file_browse_button
.
clicked
.
connect
(
self
.
_SH_TLSCAFileBrowseButtonClicked
)
self
.
tls_cert_file_editor
.
locationCleared
.
connect
(
self
.
_SH_TLSCertFileEditorLocationCleared
)
self
.
tls_cert_file_browse_button
.
clicked
.
connect
(
self
.
_SH_TLSCertFileBrowseButtonClicked
)
self
.
tls_verify_server_button
.
clicked
.
connect
(
self
.
_SH_TLSVerifyServerButtonClicked
)
# Setup initial state (show the accounts page right after start)
# Setup initial state (show the accounts page right after start)
self
.
accounts_action
.
trigger
()
self
.
accounts_action
.
trigger
()
...
@@ -445,12 +446,13 @@ class PreferencesWindow(base_class, ui_class, metaclass=QSingleton):
...
@@ -445,12 +446,13 @@ class PreferencesWindow(base_class, ui_class, metaclass=QSingleton):
# account advanced tab
# account advanced tab
font_metrics
=
self
.
register_interval_label
.
fontMetrics
()
# we assume all labels have the same font
font_metrics
=
self
.
register_interval_label
.
fontMetrics
()
# we assume all labels have the same font
labels
=
(
self
.
register_interval_label
,
self
.
publish_interval_label
,
self
.
subscribe_interval_label
,
labels
=
(
self
.
register_interval_label
,
self
.
publish_interval_label
,
self
.
subscribe_interval_label
,
self
.
idd_prefix_label
,
self
.
prefix_label
,
self
.
account_tls_cert_file_label
)
self
.
idd_prefix_label
,
self
.
prefix_label
)
text_width
=
max
(
font_metrics
.
width
(
label
.
text
())
for
label
in
labels
)
+
15
text_width
=
max
(
font_metrics
.
width
(
label
.
text
())
for
label
in
labels
)
+
15
self
.
register_interval_label
.
setMinimumWidth
(
text_width
)
self
.
register_interval_label
.
setMinimumWidth
(
text_width
)
self
.
idd_prefix_label
.
setMinimumWidth
(
text_width
)
self
.
idd_prefix_label
.
setMinimumWidth
(
text_width
)
self
.
account_
tls_cert_file_label
.
setMinimumWidth
(
text_width
)
self
.
tls_cert_file_label
.
setMinimumWidth
(
text_width
)
# audio settings
# audio settings
font_metrics
=
self
.
answer_delay_label
.
fontMetrics
()
# we assume all labels have the same font
font_metrics
=
self
.
answer_delay_label
.
fontMetrics
()
# we assume all labels have the same font
labels
=
(
self
.
audio_input_device_label
,
self
.
audio_output_device_label
,
self
.
audio_alert_device_label
,
self
.
audio_sample_rate_label
,
labels
=
(
self
.
audio_input_device_label
,
self
.
audio_output_device_label
,
self
.
audio_alert_device_label
,
self
.
audio_sample_rate_label
,
...
@@ -748,6 +750,8 @@ class PreferencesWindow(base_class, ui_class, metaclass=QSingleton):
...
@@ -748,6 +750,8 @@ class PreferencesWindow(base_class, ui_class, metaclass=QSingleton):
self
.
screenshots_directory_editor
.
setText
(
blink_settings
.
screenshots_directory
or
''
)
self
.
screenshots_directory_editor
.
setText
(
blink_settings
.
screenshots_directory
or
''
)
self
.
transfers_directory_editor
.
setText
(
blink_settings
.
transfers_directory
or
''
)
self
.
transfers_directory_editor
.
setText
(
blink_settings
.
transfers_directory
or
''
)
self
.
tls_ca_file_editor
.
setText
(
settings
.
tls
.
ca_list
or
''
)
self
.
tls_ca_file_editor
.
setText
(
settings
.
tls
.
ca_list
or
''
)
self
.
tls_cert_file_editor
.
setText
(
settings
.
tls
.
certificate
or
''
)
self
.
tls_verify_server_button
.
setChecked
(
settings
.
tls
.
verify_server
)
def
load_account_settings
(
self
,
account
):
def
load_account_settings
(
self
,
account
):
"""Load the account settings from configuration into the UI controls"""
"""Load the account settings from configuration into the UI controls"""
...
@@ -828,6 +832,8 @@ class PreferencesWindow(base_class, ui_class, metaclass=QSingleton):
...
@@ -828,6 +832,8 @@ class PreferencesWindow(base_class, ui_class, metaclass=QSingleton):
self
.
msrp_transport_button
.
setCurrentIndex
(
self
.
msrp_transport_button
.
findText
(
account
.
msrp
.
transport
.
upper
()))
self
.
msrp_transport_button
.
setCurrentIndex
(
self
.
msrp_transport_button
.
findText
(
account
.
msrp
.
transport
.
upper
()))
# Advanced tab
# Advanced tab
self
.
account_tls_name_editor
.
setText
(
account
.
sip
.
tls_name
or
account
.
id
.
domain
)
with
blocked_qt_signals
(
self
.
register_interval
):
with
blocked_qt_signals
(
self
.
register_interval
):
self
.
register_interval
.
setValue
(
account
.
sip
.
register_interval
)
self
.
register_interval
.
setValue
(
account
.
sip
.
register_interval
)
with
blocked_qt_signals
(
self
.
publish_interval
):
with
blocked_qt_signals
(
self
.
publish_interval
):
...
@@ -847,11 +853,9 @@ class PreferencesWindow(base_class, ui_class, metaclass=QSingleton):
...
@@ -847,11 +853,9 @@ class PreferencesWindow(base_class, ui_class, metaclass=QSingleton):
if
index
==
-
1
:
if
index
==
-
1
:
self
.
prefix_button
.
addItem
(
item_text
)
self
.
prefix_button
.
addItem
(
item_text
)
self
.
prefix_button
.
setCurrentIndex
(
self
.
prefix_button
.
findText
(
item_text
))
self
.
prefix_button
.
setCurrentIndex
(
self
.
prefix_button
.
findText
(
item_text
))
self
.
_update_pstn_example_label
()
self
.
_update_pstn_example_label
()
self
.
account_tls_cert_file_editor
.
setText
(
account
.
tls
.
certificate
or
''
)
self
.
account_tls_verify_server_button
.
setChecked
(
account
.
tls
.
verify_server
)
def
update_chat_preview
(
self
):
def
update_chat_preview
(
self
):
blink_settings
=
BlinkSettings
()
blink_settings
=
BlinkSettings
()
...
@@ -1055,11 +1059,6 @@ class PreferencesWindow(base_class, ui_class, metaclass=QSingleton):
...
@@ -1055,11 +1059,6 @@ class PreferencesWindow(base_class, ui_class, metaclass=QSingleton):
else
:
else
:
account_manager
.
default_account
=
None
account_manager
.
default_account
=
None
if
selected_account
.
tls
.
certificate
is
not
None
and
selected_account
.
tls
.
certificate
.
normalized
.
startswith
(
ApplicationData
.
directory
):
try
:
os
.
unlink
(
selected_account
.
tls
.
certificate
.
normalized
)
except
(
AttributeError
,
OSError
,
IOError
):
pass
selected_account
.
delete
()
selected_account
.
delete
()
...
@@ -1194,6 +1193,13 @@ class PreferencesWindow(base_class, ui_class, metaclass=QSingleton):
...
@@ -1194,6 +1193,13 @@ class PreferencesWindow(base_class, ui_class, metaclass=QSingleton):
account
.
auth
.
username
=
auth_username
account
.
auth
.
username
=
auth_username
account
.
save
()
account
.
save
()
def
_SH_TLSPeerNameEditorEditingFinished
(
self
):
account
=
self
.
selected_account
tls_name
=
self
.
account_tls_name_editor
.
text
()
or
None
if
account
.
sip
.
tls_name
!=
tls_name
:
account
.
sip
.
tls_name
=
tls_name
account
.
save
()
def
_SH_AlwaysUseMyMSRPRelayButtonClicked
(
self
,
checked
):
def
_SH_AlwaysUseMyMSRPRelayButtonClicked
(
self
,
checked
):
account
=
self
.
selected_account
account
=
self
.
selected_account
account
.
nat_traversal
.
use_msrp_relay_for_outbound
=
checked
account
.
nat_traversal
.
use_msrp_relay_for_outbound
=
checked
...
@@ -1295,19 +1301,19 @@ class PreferencesWindow(base_class, ui_class, metaclass=QSingleton):
...
@@ -1295,19 +1301,19 @@ class PreferencesWindow(base_class, ui_class, metaclass=QSingleton):
account
.
pstn
.
prefix
=
prefix
account
.
pstn
.
prefix
=
prefix
account
.
save
()
account
.
save
()
def
_SH_
Account
TLSCertFileEditorLocationCleared
(
self
):
def
_SH_TLSCertFileEditorLocationCleared
(
self
):
account
=
self
.
selected_account
settings
=
SIPSimpleSettings
()
account
.
tls
.
certificate
=
None
settings
.
tls
.
certificate
=
None
account
.
save
()
settings
.
save
()
def
_SH_
Account
TLSCertFileBrowseButtonClicked
(
self
,
checked
):
def
_SH_TLSCertFileBrowseButtonClicked
(
self
,
checked
):
# TODO: open the file selection dialog in non-modal mode (and the error messages boxes as well). -Dan
# TODO: open the file selection dialog in non-modal mode (and the error messages boxes as well). -Dan
account
=
self
.
selected_account
settings
=
SIPSimpleSettings
()
directory
=
os
.
path
.
dirname
(
account
.
tls
.
certificate
.
normalized
)
if
account
.
tls
.
certificate
else
Path
(
'~'
)
.
normalized
directory
=
os
.
path
.
dirname
(
settings
.
tls
.
certificate
.
normalized
)
if
settings
.
tls
.
certificate
else
Path
(
'~'
)
.
normalized
cert_path
=
QFileDialog
.
getOpenFileName
(
self
,
'Select Certificate File'
,
directory
,
"TLS certificates (*.crt *.pem)"
)[
0
]
or
None
cert_path
=
QFileDialog
.
getOpenFileName
(
self
,
'Select Certificate File'
,
directory
,
"TLS certificates (*.crt *.pem)"
)[
0
]
or
None
if
cert_path
is
not
None
:
if
cert_path
is
not
None
:
cert_path
=
os
.
path
.
normpath
(
cert_path
)
cert_path
=
os
.
path
.
normpath
(
cert_path
)
if
cert_path
!=
account
.
tls
.
certificate
:
if
cert_path
!=
settings
.
tls
.
certificate
:
try
:
try
:
contents
=
open
(
cert_path
)
.
read
()
contents
=
open
(
cert_path
)
.
read
()
X509Certificate
(
contents
)
X509Certificate
(
contents
)
...
@@ -1317,14 +1323,14 @@ class PreferencesWindow(base_class, ui_class, metaclass=QSingleton):
...
@@ -1317,14 +1323,14 @@ class PreferencesWindow(base_class, ui_class, metaclass=QSingleton):
except
GNUTLSError
as
e
:
except
GNUTLSError
as
e
:
QMessageBox
.
critical
(
self
,
"TLS Certificate Error"
,
"The certificate file is invalid:
%
s"
%
e
)
QMessageBox
.
critical
(
self
,
"TLS Certificate Error"
,
"The certificate file is invalid:
%
s"
%
e
)
else
:
else
:
self
.
account_
tls_cert_file_editor
.
setText
(
cert_path
)
self
.
tls_cert_file_editor
.
setText
(
cert_path
)
account
.
tls
.
certificate
=
cert_path
settings
.
tls
.
certificate
=
cert_path
account
.
save
()
settings
.
save
()
def
_SH_
Account
TLSVerifyServerButtonClicked
(
self
,
checked
):
def
_SH_TLSVerifyServerButtonClicked
(
self
,
checked
):
account
=
self
.
selected_account
settings
=
SIPSimpleSettings
()
account
.
tls
.
verify_server
=
checked
settings
.
tls
.
verify_server
=
checked
account
.
save
()
settings
.
save
()
# Audio devices signal handlers
# Audio devices signal handlers
def
_SH_AudioAlertDeviceButtonActivated
(
self
,
index
):
def
_SH_AudioAlertDeviceButtonActivated
(
self
,
index
):
...
...
blink/sessions.py
View file @
b3a8f270
...
@@ -741,7 +741,7 @@ class BlinkSession(BlinkSessionBase):
...
@@ -741,7 +741,7 @@ class BlinkSession(BlinkSessionBase):
self.lookup = DNSLookup()
self.lookup = DNSLookup()
notification_center.add_observer(self, sender=self.lookup)
notification_center.add_observer(self, sender=self.lookup)
self
.
lookup
.
lookup_sip_proxy
(
uri
,
settings
.
sip
.
transport_list
)
self.lookup.lookup_sip_proxy(uri, settings.sip.transport_list
, tls_name=self.account.sip.tls_name or uri.host
)
def add_stream(self, stream_description):
def add_stream(self, stream_description):
self.add_streams([stream_description])
self.add_streams([stream_description])
...
@@ -3937,7 +3937,7 @@ class BlinkFileTransfer(BlinkSessionBase):
...
@@ -3937,7 +3937,7 @@ class BlinkFileTransfer(BlinkSessionBase):
lookup = DNSLookup()
lookup = DNSLookup()
notification_center.add_observer(self, sender=lookup)
notification_center.add_observer(self, sender=lookup)
lookup
.
lookup_sip_proxy
(
uri
,
settings
.
sip
.
transport_list
)
lookup.lookup_sip_proxy(uri, settings.sip.transport_list
, tls_name=self.account.sip.tls_name or uri.host
)
self.state = 'connecting/dns_lookup'
self.state = 'connecting/dns_lookup'
...
...
debian/control
View file @
b3a8f270
...
@@ -25,7 +25,7 @@ Depends: ${python3:Depends}, ${shlibs:Depends}, ${misc:Depends},
...
@@ -25,7 +25,7 @@ Depends: ${python3:Depends}, ${shlibs:Depends}, ${misc:Depends},
python3-pyqt5,
python3-pyqt5,
python3-pyqt5.qtsvg,
python3-pyqt5.qtsvg,
python3-pyqt5.qtwebkit,
python3-pyqt5.qtwebkit,
python3-sipsimple,
python3-sipsimple
(>=5.2.0)
,
python3-twisted,
python3-twisted,
python3-zope.interface,
python3-zope.interface,
x11vnc
x11vnc
...
...
resources/preferences.ui
View file @
b3a8f270
This diff is collapsed.
Click to expand it.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment