Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
D
domotikad
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
Wiki
Wiki
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Commits
Issue Boards
Open sidebar
domotika
domotikad
Commits
68da1e55
Commit
68da1e55
authored
Oct 20, 2016
by
Franco (nextime) Lanza
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Better code organization
parent
f4087849
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
229 additions
and
135 deletions
+229
-135
dmdb.py
domotika/db/dmdb.py
+1
-1
domotika.py
domotika/domotika.py
+7
-4
bot.py
domotika/web/bot.py
+221
-130
No files found.
domotika/db/dmdb.py
View file @
68da1e55
...
...
@@ -941,7 +941,7 @@ def getVoiceCommandList():
def
getScreenshotUri
(
target
):
qstring
=
"select screenshot from mediasources where button_name LIKE '"
+
target
+
"
%
'
limit 1
"
qstring
=
"select screenshot from mediasources where button_name LIKE '"
+
target
+
"
%
'"
return
runQuery
(
qstring
)
def
getClimaUniques
():
...
...
domotika/domotika.py
View file @
68da1e55
...
...
@@ -2455,13 +2455,16 @@ class domotikaService(service.Service):
def
web_on_getScreenshotList
(
self
,
screenshot
=
True
):
return
dmdb
.
getScreenshotList
(
screenshot
=
screenshot
)
def
web_on_getScreenshot
(
self
,
target
):
def
web_on_getScreenshot
(
self
,
target
,
callback
=
None
):
def
imageReturn
(
img
):
return
img
def
prepareScreenshot
(
res
):
if
res
:
return
wu
.
getPage
(
res
[
0
][
0
])
.
addCallback
(
imageReturn
)
return
False
if
callback
==
None
:
if
res
:
return
wu
.
getPage
(
res
[
0
][
0
])
.
addCallback
(
imageReturn
)
return
False
for
r
in
res
:
wu
.
getPage
(
r
[
0
])
.
addCallback
(
callback
)
return
dmdb
.
getScreenshotUri
(
target
)
.
addCallback
(
prepareScreenshot
)
def
web_on_getClimaUniques
(
self
):
...
...
domotika/web/bot.py
View file @
68da1e55
from
zope.interface
import
Interface
,
implements
from
twisted.python
import
components
from
nevow
import
rend
,
inevow
from
corepost
import
Response
,
NotFoundException
,
AlreadyExistsException
...
...
@@ -16,6 +17,8 @@ from Queue import Queue
import
uuid
from
domotika.singleton
import
messengerlinks
from
StringIO
import
StringIO
import
imghdr
_MESSENGER_LINKS
=
messengerlinks
.
MessengerLinkRegistry
()
_MESSENGER_PSID
=
messengerlinks
.
MessengerPSIDRegistry
()
...
...
@@ -33,6 +36,114 @@ log = logging.getLogger( 'Webgui' )
BOTResource
=
RESTResource
class
IBotProtoInterface
(
Interface
):
def
getUsername
(
self
,
uid
):
""" get a username from uid """
def
isLogged
(
self
,
uid
):
""" return True if it's a know user """
def
sendAPI
(
self
,
payload
,
req_uri
):
""" Send request to the API """
def
receivedAuthentication
(
self
,
msg
):
""" receive auth message """
def
receivedTextMessage
(
self
,
msg
):
""" receive text message """
def
receivedDeliveryConfirmation
(
self
,
msg
):
""" receive delivery confirmation """
def
receivedPostback
(
self
,
msg
):
""" receive a postback message """
def
receivedAccountLink
(
self
,
msg
):
""" receive account linking message """
def
sendCommandList
(
self
,
recipient_id
):
""" send command list to the user """
def
sendScreenshotList
(
self
,
recipient_id
):
""" send a list of screenshots available """
def
sendScreenshot
(
self
,
recipient_id
,
target
):
""" send one or more screenshots """
def
sendClima
(
self
,
recipient_id
):
""" send Clima status """
def
sendMessage
(
self
,
recipient_id
,
message
):
""" send a text message """
def
sendImageMessage
(
self
,
recipient_id
,
imguri
):
""" send an image link """
def
sendImageMessageData
(
self
,
recipient_id
,
imagedata
,
mtype
):
""" send an image by data """
def
sendAuthRequest
(
self
,
recipient_id
):
""" send login request """
class
BotProto
(
object
):
def
__init__
(
self
,
core
):
self
.
core
=
core
def
botCommandParser
(
self
,
uid
,
txt
):
def
voiceResult
(
res
):
log
.
info
(
'VoiceResult: '
+
str
(
res
))
if
len
(
res
)
>
0
:
if
res
[
0
]
==
'Ok'
and
len
(
res
)
>
1
:
try
:
result
=
'ho eseguito "'
+
" "
.
join
(
res
[
1
][
'clean'
])
+
'"'
except
:
pass
else
:
result
=
'Spiacente, non ho trovato un comando corrispondente'
self
.
sendMessage
(
uid
,
result
)
txt
=
unicode
(
txt
.
lower
())
if
not
self
.
isLogged
(
uid
):
if
txt
==
u'hello'
or
txt
==
u'ciao'
:
self
.
sendMessage
(
uid
,
'Hello, how can i help you?'
)
elif
txt
==
u'?'
or
txt
==
u'help'
:
self
.
sendMessage
(
uid
,
'you can
\'
t do that.'
)
elif
txt
==
u'login'
:
self
.
sendAuthRequest
(
uid
)
else
:
self
.
sendMessage
(
uid
,
txt
+
u" come se fosse antani"
)
else
:
if
txt
==
u'hello'
or
txt
==
u'ciao'
:
self
.
sendMessage
(
uid
,
'Hello
%
s, how can i help you?'
%
(
self
.
getUsername
(
uid
)))
elif
txt
==
u'?'
or
txt
==
u'help'
:
self
.
sendMessage
(
uid
,
'Ok, devo ancora implementare l
\'
aiuto! Sorry for that!'
)
self
.
sendMessage
(
uid
,
'Anyway, i comandi che hai sono: "command list", "screenshot list", "screenshot" e "clima".'
)
self
.
sendMessage
(
uid
,
'Qualsiasi altro comando viene interpretato come un potenziale comando vocale.'
)
elif
txt
==
u'logout'
:
self
.
sendMessage
(
uid
,
'Ok, devo ancora implementare anche il logout'
)
elif
txt
==
u'login'
:
self
.
sendMessage
(
uid
,
'Sei gia
\'
loggato,
%
s!'
%
(
self
.
getUsername
(
uid
)))
elif
txt
==
u'command list'
or
txt
==
'cmd list'
:
self
.
sendCommandList
(
uid
)
elif
txt
==
u'screenshot list'
or
txt
==
u'ss list'
:
self
.
sendScreenshotList
(
uid
)
elif
txt
.
startswith
(
'screenshot '
)
or
txt
.
startswith
(
'ss '
):
self
.
sendScreenshot
(
uid
,
" "
.
join
(
txt
.
split
()[
1
:]))
elif
txt
==
u'clima'
:
self
.
sendClima
(
uid
)
else
:
self
.
core
.
voiceReceived
(
txt
,
confidence
=
1.0
,
lang
=
"it"
)
.
addCallback
(
voiceResult
)
class
BotCore
(
object
):
path
=
""
...
...
@@ -76,26 +187,30 @@ class MessengerMessage(object):
self
.
payload
=
payload
self
.
uri
=
uri
class
MessengerCo
re
(
object
):
class
MessengerCo
nf
(
object
):
def
_cfgGet
(
self
,
keyword
):
return
self
.
core
.
configGet
(
'messenger'
,
keyword
)
class
MessengerBot
(
BotCore
,
MessengerCore
):
class
MessengerBotAdapter
(
MessengerConf
):
implements
(
IBotProtoInterface
)
path
=
"messenger"
graphuri
=
'https://graph.facebook.com/v2.6'
sendQueue
=
Queue
()
sendlock
=
False
#graphuri = 'http://192.168.4.2/v2.6'
def
__init__
(
self
,
orig
):
self
.
orig
=
orig
self
.
core
=
orig
.
core
@
property
def
auth_args
(
self
):
if
not
hasattr
(
self
,
'_auth_args'
):
auth
=
{
'access_token'
:
unicode
(
self
.
_cfgGet
(
'page_token'
))
}
}
if
self
.
_cfgGet
(
'app_secret'
)
is
not
None
and
len
(
self
.
_cfgGet
(
'app_secret'
))
>
0
:
appsecret_proof
=
self
.
_generateAppSecProof
()
auth
[
'appsecret_proof'
]
=
appsecret_proof
...
...
@@ -109,6 +224,7 @@ class MessengerBot(BotCore, MessengerCore):
hmac_object
=
hmac
.
new
(
bytearray
(
self
.
_cfgGet
(
'app_secret'
),
'utf8'
),
str
(
self
.
_cfgGet
(
'page_token'
))
.
encode
(
'utf8'
),
hashlib
.
sha256
)
return
hmac_object
.
hexdigest
()
def
_dataSent
(
self
,
res
):
log
.
debug
(
'Messenger BOT Datasent OK'
)
log
.
debug
(
res
)
...
...
@@ -125,10 +241,8 @@ class MessengerBot(BotCore, MessengerCore):
def
_sendRaw
(
self
):
if
not
self
.
sendQueue
.
empty
()
and
not
self
.
sendLock
:
message
=
self
.
sendQueue
.
get
()
return
wu
.
getPage
(
message
.
uri
,
method
=
'POST'
,
headers
=
message
.
headers
,
postdata
=
message
.
payload
)
.
addCallbacks
(
self
.
_dataSent
,
self
.
_dataError
)
return
wu
.
getPage
(
message
.
uri
,
method
=
'POST'
,
headers
=
message
.
headers
,
postdata
=
message
.
payload
)
.
addCallbacks
(
self
.
_dataSent
,
self
.
_dataError
)
def
sendAPI
(
self
,
payload
,
req_uri
=
'/me/messages'
):
request_endpoint
=
self
.
graphuri
+
req_uri
...
...
@@ -137,112 +251,23 @@ class MessengerBot(BotCore, MessengerCore):
ctype
=
{
"Content-Type"
:
"application/json"
}
return
wu
.
getPage
(
request_uri
,
method
=
'POST'
,
headers
=
ctype
,
postdata
=
convertToJson
(
payload
))
.
addCallbacks
(
self
.
_dataSent
,
self
.
_dataError
)
def
getUsername
(
self
,
uid
):
return
_MESSENGER_PSID
.
get_link
(
uid
)
@
route
(
"/"
,
Http
.
GET
)
def
rootGet
(
self
,
request
,
*
a
,
**
kw
):
if
'hub.mode'
in
kw
.
keys
()
and
kw
[
'hub.mode'
]
==
'subscribe'
and
'hub.challenge'
in
kw
.
keys
():
if
'hub.verify_token'
in
kw
.
keys
()
and
kw
[
'hub.verify_token'
]
==
self
.
_cfgGet
(
'verify_token'
):
log
.
info
(
"New verification request for Messenger BOT: "
+
str
(
kw
))
return
kw
[
'hub.challenge'
]
return
Response
(
403
,
"Failed validation."
)
@
route
(
"/"
,
Http
.
POST
)
@
messengerValidator
()
def
rootPost
(
self
,
request
,
*
a
,
**
kw
):
log
.
debug
(
"New messenger bot request: "
+
str
(
kw
))
if
'object'
in
kw
.
keys
()
and
kw
[
'object'
]
==
'page'
and
'entry'
in
kw
.
keys
():
for
entry
in
kw
[
'entry'
]:
try
:
#if True:
pageID
=
entry
[
'id'
]
timestamp
=
entry
[
'time'
]
messaging
=
entry
[
'messaging'
]
for
message
in
messaging
:
msgkeys
=
message
.
keys
()
if
'optin'
in
msgkeys
:
self
.
receivedAuthentication
(
message
)
elif
'message'
in
msgkeys
:
self
.
receivedMessage
(
message
)
elif
'delivery'
in
msgkeys
:
self
.
receivedDeliveryConfirmation
(
message
)
elif
'postback'
in
msgkeys
:
self
.
receivedPostback
(
message
)
elif
'read'
in
msgkeys
:
self
.
receivedMessageRead
(
message
)
elif
'account_linking'
in
msgkeys
:
self
.
receivedAccountLink
(
message
)
else
:
log
.
info
(
"Received unknow messaging webhook for messenger BOT: "
+
str
(
kw
))
except
:
pass
return
Response
(
200
,
'OK'
)
@
route
(
"/loginfailed"
)
def
loginFailed
(
self
,
request
,
*
a
,
**
kw
):
k
=
kw
.
keys
()
if
'account_linking_token'
in
k
and
'redirect_uri'
in
k
:
log
.
info
(
"Passed linking uri "
+
kw
[
'redirect_uri'
])
luri
=
kw
[
'redirect_uri'
]
log
.
info
(
"Authorization failed: redirecting to "
+
luri
)
return
Response
(
302
,
'Authorization failed'
,
headers
=
{
'Location'
:
luri
})
return
Response
(
200
,
"Authorization failed."
)
def
isLogged
(
self
,
uid
):
return
_MESSENGER_PSID
.
linkid_exists
(
uid
)
def
receivedAuthentication
(
self
,
msg
):
log
.
info
(
"Messenger bot received authentication: "
+
str
(
msg
))
def
receivedMessage
(
self
,
msg
):
def
voiceResult
(
res
):
log
.
info
(
'VoiceResult: '
+
str
(
res
))
if
len
(
res
)
>
0
:
if
res
[
0
]
==
'Ok'
and
len
(
res
)
>
1
:
try
:
result
=
'ho eseguito "'
+
" "
.
join
(
res
[
1
][
'clean'
])
+
'"'
except
:
pass
else
:
result
=
'Spiacente, non ho trovato un comando corrispondente'
self
.
sendMessage
(
msg
[
'sender'
][
'id'
],
result
)
def
receivedTextMessage
(
self
,
msg
):
log
.
info
(
"Messenger bot received message: "
+
str
(
msg
))
message
=
u'Received: '
+
unicode
(
msg
[
'message'
][
'text'
])
txt
=
unicode
(
msg
[
'message'
][
'text'
])
senderid
=
msg
[
'sender'
][
'id'
]
if
not
'is_echo'
in
msg
[
'message'
]
.
keys
()
or
not
msg
[
'message'
][
'is_echo'
]:
if
not
_MESSENGER_PSID
.
linkid_exists
(
senderid
):
log
.
info
(
_MESSENGER_PSID
.
links
)
if
txt
==
u'hello'
or
txt
==
u'ciao'
:
self
.
sendMessage
(
senderid
,
'Hello, how can i help you?'
)
elif
txt
==
u'?'
or
txt
==
u'help'
:
self
.
sendMessage
(
senderid
,
'you can
\'
t do that.'
)
elif
txt
==
u'login'
:
self
.
sendAuthRequest
(
senderid
)
else
:
self
.
sendMessage
(
senderid
,
txt
+
" come se fosse antani"
)
else
:
if
txt
==
u'hello'
or
txt
==
u'ciao'
:
self
.
sendMessage
(
senderid
,
'Hello
%
s, how can i help you?'
%
(
_MESSENGER_PSID
.
get_link
(
senderid
)))
elif
txt
==
u'?'
or
txt
==
u'help'
:
self
.
sendMessage
(
senderid
,
'Ok, devo ancora implementare l
\'
aiuto! Sorry for that!'
)
self
.
sendMessage
(
senderid
,
'Anyway, i comandi che hai sono: "command list", "screenshot list", "screenshot" e "clima".'
)
self
.
sendMessage
(
senderid
,
'Qualsiasi altro comando viene interpretato come un potenziale comando vocale.'
)
elif
txt
==
u'logout'
:
self
.
sendMessage
(
senderid
,
'Ok, devo ancora implementare anche il logout'
)
elif
txt
==
u'command list'
or
txt
==
'cmd list'
:
self
.
sendCommandList
(
senderid
)
elif
txt
==
u'screenshot list'
or
txt
==
u'ss list'
:
self
.
sendScreenshotList
(
senderid
)
elif
txt
.
startswith
(
'screenshot '
)
or
txt
.
startswith
(
'ss '
):
self
.
sendScreenshot
(
senderid
,
" "
.
join
(
txt
.
split
()[
1
:]))
elif
txt
==
u'clima'
:
self
.
sendClima
(
senderid
)
else
:
self
.
core
.
voiceReceived
(
txt
,
confidence
=
1.0
,
lang
=
"it"
)
.
addCallback
(
voiceResult
)
message
=
u'Received: '
+
unicode
(
msg
[
'message'
][
'text'
])
txt
=
unicode
(
msg
[
'message'
][
'text'
])
.
lower
()
botCommandParser
(
self
,
senderid
,
txt
)
def
receivedDeliveryConfirmation
(
self
,
msg
):
log
.
info
(
"Messenger bot received delivery confirmation: "
+
str
(
msg
))
...
...
@@ -280,13 +305,13 @@ class MessengerBot(BotCore, MessengerCore):
def
sendScreenshot
(
self
,
recipient_id
,
target
):
def
pushImage
(
res
):
if
res
:
self
.
sendImageMessageData
(
recipient_id
,
res
)
self
.
sendImageMessageData
(
recipient_id
,
res
)
else
:
self
.
sendMessage
(
recipient_id
,
'Cannot retrieve image of '
+
str
(
target
))
if
target
.
endswith
(
'
%
'
):
target
=
target
[:
-
1
]
if
len
(
target
)
>
0
:
self
.
core
.
getScreenshot
(
target
)
.
addCallback
(
pushImage
)
self
.
core
.
getScreenshot
(
target
,
pushImage
)
def
sendClima
(
self
,
recipient_id
):
def
pushClima
(
res
):
...
...
@@ -318,7 +343,7 @@ class MessengerBot(BotCore, MessengerCore):
payload
=
{
'recipient'
:
{
'id'
:
recipient_id
},
},
'message'
:
{
'attachment'
:
{
'type'
:
'image'
,
...
...
@@ -330,19 +355,22 @@ class MessengerBot(BotCore, MessengerCore):
}
return
self
.
sendAPI
(
payload
)
def
sendImageMessageData
(
self
,
recipient_id
,
imagedata
):
payload
=
{
'recipient'
:
{
'id'
:
recipient_id
},
'message'
:
{
'attachment'
:
{
'type'
:
'image'
,
'payload'
:
{}
}
}
}
fname
=
str
(
uuid
.
uuid4
()
.
get_hex
())
+
'.jpg'
def
sendImageMessageData
(
self
,
recipient_id
,
imagedata
,
mtype
=
None
):
if
not
imagedata
:
return
if
mtype
==
None
:
ft
=
imghdr
.
what
(
None
,
imagedata
)
log
.
info
(
"FILE TYPE: "
+
str
(
ft
))
if
ft
in
[
'jpeg'
,
'gif'
,
'png'
]:
mtype
=
ft
else
:
log
.
warning
(
"Image mimetype doesn't seems to be valid: "
+
str
(
ft
))
log
.
warning
(
"Assuming it's a jpeg."
)
mtype
=
'jpeg'
fname
=
str
(
uuid
.
uuid4
()
.
get_hex
())
+
mtype
bond
=
"------------------------"
+
str
(
uuid
.
uuid4
()
.
get_hex
())[:
16
]
data
=
"--"
+
bond
+
"
\r\n
"
data
+=
"Content-Disposition: form-data; name=
\"
recipient
\"\r\n\r\n
"
...
...
@@ -352,7 +380,7 @@ class MessengerBot(BotCore, MessengerCore):
data
+=
'{"attachment":{"type":"image", "payload":{}}}'
+
"
\r\n
"
data
+=
"--"
+
bond
+
"
\r\n
"
data
+=
'Content-Disposition: form-data; name="filedata"; filename="'
+
fname
+
'"'
+
"
\r\n
"
data
+=
'Content-Type: image/
jpeg'
+
"
\r\n\r\n
"
data
+=
'Content-Type: image/
'
+
mtype
+
"
\r\n\r\n
"
data
+=
str
(
imagedata
)
data
+=
"
\r\n
--"
+
bond
+
"--
\r\n
"
headers
=
{
...
...
@@ -362,17 +390,16 @@ class MessengerBot(BotCore, MessengerCore):
request_endpoint
=
self
.
graphuri
+
'/me/messages'
request_uri
=
request_endpoint
+
'?'
+
urlencode
(
self
.
auth_args
)
return
wu
.
getPage
(
request_uri
,
agent
=
"curl/7.50.1"
,
method
=
'POST'
,
headers
=
headers
,
return
wu
.
getPage
(
request_uri
,
agent
=
"curl/7.50.1"
,
method
=
'POST'
,
headers
=
headers
,
postdata
=
data
,
expect100
=
True
)
.
addCallbacks
(
self
.
_dataSent
,
self
.
_dataError
)
def
sendAuthRequest
(
self
,
recipient_id
):
payload
=
{
'recipient'
:
{
'id'
:
recipient_id
},
},
'message'
:
{
"attachment"
:
{
"attachment"
:
{
"type"
:
"template"
,
"payload"
:
{
"template_type"
:
"generic"
,
...
...
@@ -389,9 +416,73 @@ class MessengerBot(BotCore, MessengerCore):
}
}
return
self
.
sendAPI
(
payload
)
components
.
registerAdapter
(
MessengerBotAdapter
,
BotProto
,
IBotProtoInterface
)
class
MessengerBot
(
BotCore
,
MessengerConf
):
path
=
"messenger"
@
route
(
"/"
,
Http
.
GET
)
def
rootGet
(
self
,
request
,
*
a
,
**
kw
):
if
'hub.mode'
in
kw
.
keys
()
and
kw
[
'hub.mode'
]
==
'subscribe'
and
'hub.challenge'
in
kw
.
keys
():
if
'hub.verify_token'
in
kw
.
keys
()
and
kw
[
'hub.verify_token'
]
==
self
.
_cfgGet
(
'verify_token'
):
log
.
info
(
"New verification request for Messenger BOT: "
+
str
(
kw
))
return
kw
[
'hub.challenge'
]
return
Response
(
403
,
"Failed validation."
)
@
route
(
"/"
,
Http
.
POST
)
@
messengerValidator
()
def
rootPost
(
self
,
request
,
*
a
,
**
kw
):
log
.
debug
(
"New messenger bot request: "
+
str
(
kw
))
botproto
=
BotProto
(
self
.
core
)
botiface
=
IBotProtoInterface
(
botproto
)
if
'object'
in
kw
.
keys
()
and
kw
[
'object'
]
==
'page'
and
'entry'
in
kw
.
keys
():
for
entry
in
kw
[
'entry'
]:
#try:
if
True
:
pageID
=
entry
[
'id'
]
timestamp
=
entry
[
'time'
]
messaging
=
entry
[
'messaging'
]
for
message
in
messaging
:
msgkeys
=
message
.
keys
()
if
'optin'
in
msgkeys
:
botiface
.
receivedAuthentication
(
message
)
elif
'message'
in
msgkeys
:
botiface
.
receivedTextMessage
(
message
)
elif
'delivery'
in
msgkeys
:
botiface
.
receivedDeliveryConfirmation
(
message
)
elif
'postback'
in
msgkeys
:
botiface
.
receivedPostback
(
message
)
elif
'read'
in
msgkeys
:
botiface
.
receivedMessageRead
(
message
)
elif
'account_linking'
in
msgkeys
:
botiface
.
receivedAccountLink
(
message
)
else
:
log
.
info
(
"Received unknow messaging webhook for messenger BOT: "
+
str
(
kw
))
#except:
# pass
return
Response
(
200
,
'OK'
)
@
route
(
"/loginfailed"
)
def
loginFailed
(
self
,
request
,
*
a
,
**
kw
):
k
=
kw
.
keys
()
if
'account_linking_token'
in
k
and
'redirect_uri'
in
k
:
log
.
info
(
"Passed linking uri "
+
kw
[
'redirect_uri'
])
luri
=
kw
[
'redirect_uri'
]
log
.
info
(
"Authorization failed: redirecting to "
+
luri
)
return
Response
(
302
,
'Authorization failed'
,
headers
=
{
'Location'
:
luri
})
return
Response
(
200
,
"Authorization failed."
)
class
MessengerBotAuth
(
BotCore
,
MessengerCo
re
):
class
MessengerBotAuth
(
BotCore
,
MessengerCo
nf
):
path
=
"messenger"
...
...
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