2019-11-21 16:19:22 +00:00
package protocol
2019-07-17 22:25:42 +00:00
import (
2019-07-30 18:39:16 +00:00
"bytes"
2019-07-17 22:25:42 +00:00
"context"
"database/sql"
2019-07-30 18:39:16 +00:00
"encoding/gob"
"encoding/hex"
2019-07-17 22:25:42 +00:00
"github.com/pkg/errors"
2019-11-23 17:57:05 +00:00
"github.com/status-im/status-go/eth-node/crypto"
2019-07-17 22:25:42 +00:00
)
var (
// ErrMsgAlreadyExist returned if msg already exist.
ErrMsgAlreadyExist = errors . New ( "message with given ID already exist" )
)
// sqlitePersistence wrapper around sql db with operations common for a client.
type sqlitePersistence struct {
db * sql . DB
}
Move to protobuf for Message type (#1706)
* Use a single Message type `v1/message.go` and `message.go` are the same now, and they embed `protobuf.ChatMessage`
* Use `SendChatMessage` for sending chat messages, this is basically the old `Send` but a bit more flexible so we can send different message types (stickers,commands), and not just text.
* Remove dedup from services/shhext. Because now we process in status-protocol, dedup makes less sense, as those messages are going to be processed anyway, so removing for now, we can re-evaluate if bringing it to status-go or not.
* Change the various retrieveX method to a single one:
`RetrieveAll` will be processing those messages that it can process (Currently only `Message`), and return the rest in `RawMessages` (still transit). The format for the response is:
`Chats`: -> The chats updated by receiving the message
`Messages`: -> The messages retrieved (already matched to a chat)
`Contacts`: -> The contacts updated by the messages
`RawMessages` -> Anything else that can't be parsed, eventually as we move everything to status-protocol-go this will go away.
2019-12-05 16:25:34 +00:00
func ( db sqlitePersistence ) SaveChat ( chat Chat ) error {
return db . saveChat ( nil , chat )
}
2019-07-17 22:25:42 +00:00
Move to protobuf for Message type (#1706)
* Use a single Message type `v1/message.go` and `message.go` are the same now, and they embed `protobuf.ChatMessage`
* Use `SendChatMessage` for sending chat messages, this is basically the old `Send` but a bit more flexible so we can send different message types (stickers,commands), and not just text.
* Remove dedup from services/shhext. Because now we process in status-protocol, dedup makes less sense, as those messages are going to be processed anyway, so removing for now, we can re-evaluate if bringing it to status-go or not.
* Change the various retrieveX method to a single one:
`RetrieveAll` will be processing those messages that it can process (Currently only `Message`), and return the rest in `RawMessages` (still transit). The format for the response is:
`Chats`: -> The chats updated by receiving the message
`Messages`: -> The messages retrieved (already matched to a chat)
`Contacts`: -> The contacts updated by the messages
`RawMessages` -> Anything else that can't be parsed, eventually as we move everything to status-protocol-go this will go away.
2019-12-05 16:25:34 +00:00
func ( db sqlitePersistence ) SaveChats ( chats [ ] * Chat ) error {
tx , err := db . db . BeginTx ( context . Background ( ) , & sql . TxOptions { } )
defer func ( ) {
if err == nil {
err = tx . Commit ( )
return
}
// don't shadow original error
_ = tx . Rollback ( )
} ( )
for _ , chat := range chats {
err := db . saveChat ( tx , * chat )
if err != nil {
return err
}
2019-07-17 22:25:42 +00:00
}
Move to protobuf for Message type (#1706)
* Use a single Message type `v1/message.go` and `message.go` are the same now, and they embed `protobuf.ChatMessage`
* Use `SendChatMessage` for sending chat messages, this is basically the old `Send` but a bit more flexible so we can send different message types (stickers,commands), and not just text.
* Remove dedup from services/shhext. Because now we process in status-protocol, dedup makes less sense, as those messages are going to be processed anyway, so removing for now, we can re-evaluate if bringing it to status-go or not.
* Change the various retrieveX method to a single one:
`RetrieveAll` will be processing those messages that it can process (Currently only `Message`), and return the rest in `RawMessages` (still transit). The format for the response is:
`Chats`: -> The chats updated by receiving the message
`Messages`: -> The messages retrieved (already matched to a chat)
`Contacts`: -> The contacts updated by the messages
`RawMessages` -> Anything else that can't be parsed, eventually as we move everything to status-protocol-go this will go away.
2019-12-05 16:25:34 +00:00
return nil
2019-07-17 22:25:42 +00:00
}
Move to protobuf for Message type (#1706)
* Use a single Message type `v1/message.go` and `message.go` are the same now, and they embed `protobuf.ChatMessage`
* Use `SendChatMessage` for sending chat messages, this is basically the old `Send` but a bit more flexible so we can send different message types (stickers,commands), and not just text.
* Remove dedup from services/shhext. Because now we process in status-protocol, dedup makes less sense, as those messages are going to be processed anyway, so removing for now, we can re-evaluate if bringing it to status-go or not.
* Change the various retrieveX method to a single one:
`RetrieveAll` will be processing those messages that it can process (Currently only `Message`), and return the rest in `RawMessages` (still transit). The format for the response is:
`Chats`: -> The chats updated by receiving the message
`Messages`: -> The messages retrieved (already matched to a chat)
`Contacts`: -> The contacts updated by the messages
`RawMessages` -> Anything else that can't be parsed, eventually as we move everything to status-protocol-go this will go away.
2019-12-05 16:25:34 +00:00
func ( db sqlitePersistence ) saveChat ( tx * sql . Tx , chat Chat ) error {
2019-07-30 18:39:16 +00:00
var err error
Move to protobuf for Message type (#1706)
* Use a single Message type `v1/message.go` and `message.go` are the same now, and they embed `protobuf.ChatMessage`
* Use `SendChatMessage` for sending chat messages, this is basically the old `Send` but a bit more flexible so we can send different message types (stickers,commands), and not just text.
* Remove dedup from services/shhext. Because now we process in status-protocol, dedup makes less sense, as those messages are going to be processed anyway, so removing for now, we can re-evaluate if bringing it to status-go or not.
* Change the various retrieveX method to a single one:
`RetrieveAll` will be processing those messages that it can process (Currently only `Message`), and return the rest in `RawMessages` (still transit). The format for the response is:
`Chats`: -> The chats updated by receiving the message
`Messages`: -> The messages retrieved (already matched to a chat)
`Contacts`: -> The contacts updated by the messages
`RawMessages` -> Anything else that can't be parsed, eventually as we move everything to status-protocol-go this will go away.
2019-12-05 16:25:34 +00:00
if tx == nil {
tx , err = db . db . BeginTx ( context . Background ( ) , & sql . TxOptions { } )
if err != nil {
return err
}
defer func ( ) {
if err == nil {
err = tx . Commit ( )
return
}
// don't shadow original error
_ = tx . Rollback ( )
} ( )
}
2019-07-30 18:39:16 +00:00
pkey := [ ] byte { }
// For one to one chatID is an encoded public key
if chat . ChatType == ChatTypeOneToOne {
pkey , err = hex . DecodeString ( chat . ID [ 2 : ] )
if err != nil {
return err
}
// Safety check, make sure is well formed
_ , err := crypto . UnmarshalPubkey ( pkey )
if err != nil {
return err
}
}
// Encode members
var encodedMembers bytes . Buffer
memberEncoder := gob . NewEncoder ( & encodedMembers )
if err := memberEncoder . Encode ( chat . Members ) ; err != nil {
return err
}
// Encode membership updates
var encodedMembershipUpdates bytes . Buffer
membershipUpdatesEncoder := gob . NewEncoder ( & encodedMembershipUpdates )
if err := membershipUpdatesEncoder . Encode ( chat . MembershipUpdates ) ; err != nil {
return err
}
// Insert record
Move to protobuf for Message type (#1706)
* Use a single Message type `v1/message.go` and `message.go` are the same now, and they embed `protobuf.ChatMessage`
* Use `SendChatMessage` for sending chat messages, this is basically the old `Send` but a bit more flexible so we can send different message types (stickers,commands), and not just text.
* Remove dedup from services/shhext. Because now we process in status-protocol, dedup makes less sense, as those messages are going to be processed anyway, so removing for now, we can re-evaluate if bringing it to status-go or not.
* Change the various retrieveX method to a single one:
`RetrieveAll` will be processing those messages that it can process (Currently only `Message`), and return the rest in `RawMessages` (still transit). The format for the response is:
`Chats`: -> The chats updated by receiving the message
`Messages`: -> The messages retrieved (already matched to a chat)
`Contacts`: -> The contacts updated by the messages
`RawMessages` -> Anything else that can't be parsed, eventually as we move everything to status-protocol-go this will go away.
2019-12-05 16:25:34 +00:00
stmt , err := tx . Prepare ( ` INSERT INTO chats ( id , name , color , active , type , timestamp , deleted_at_clock_value , public_key , unviewed_message_count , last_clock_value , last_message , members , membership_updates )
VALUES ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? ) ` )
2019-07-30 18:39:16 +00:00
if err != nil {
return err
}
defer stmt . Close ( )
_ , err = stmt . Exec (
2019-08-20 11:20:25 +00:00
chat . ID ,
2019-07-30 18:39:16 +00:00
chat . Name ,
chat . Color ,
chat . Active ,
chat . ChatType ,
chat . Timestamp ,
chat . DeletedAtClockValue ,
pkey ,
chat . UnviewedMessagesCount ,
chat . LastClockValue ,
Move to protobuf for Message type (#1706)
* Use a single Message type `v1/message.go` and `message.go` are the same now, and they embed `protobuf.ChatMessage`
* Use `SendChatMessage` for sending chat messages, this is basically the old `Send` but a bit more flexible so we can send different message types (stickers,commands), and not just text.
* Remove dedup from services/shhext. Because now we process in status-protocol, dedup makes less sense, as those messages are going to be processed anyway, so removing for now, we can re-evaluate if bringing it to status-go or not.
* Change the various retrieveX method to a single one:
`RetrieveAll` will be processing those messages that it can process (Currently only `Message`), and return the rest in `RawMessages` (still transit). The format for the response is:
`Chats`: -> The chats updated by receiving the message
`Messages`: -> The messages retrieved (already matched to a chat)
`Contacts`: -> The contacts updated by the messages
`RawMessages` -> Anything else that can't be parsed, eventually as we move everything to status-protocol-go this will go away.
2019-12-05 16:25:34 +00:00
chat . LastMessage ,
2019-07-30 18:39:16 +00:00
encodedMembers . Bytes ( ) ,
encodedMembershipUpdates . Bytes ( ) ,
)
if err != nil {
return err
}
return err
}
2019-08-20 11:20:25 +00:00
func ( db sqlitePersistence ) DeleteChat ( chatID string ) error {
_ , err := db . db . Exec ( "DELETE FROM chats WHERE id = ?" , chatID )
2019-07-30 18:39:16 +00:00
return err
}
2019-08-29 06:33:46 +00:00
func ( db sqlitePersistence ) Chats ( ) ( [ ] * Chat , error ) {
return db . chats ( nil )
2019-08-20 11:20:25 +00:00
}
2019-07-30 18:39:16 +00:00
2019-11-15 08:52:28 +00:00
func ( db sqlitePersistence ) chats ( tx * sql . Tx ) ( chats [ ] * Chat , err error ) {
2019-08-20 11:20:25 +00:00
if tx == nil {
tx , err = db . db . BeginTx ( context . Background ( ) , & sql . TxOptions { } )
if err != nil {
2019-11-15 08:52:28 +00:00
return
2019-08-20 11:20:25 +00:00
}
defer func ( ) {
if err == nil {
err = tx . Commit ( )
return
}
// don't shadow original error
_ = tx . Rollback ( )
} ( )
}
2019-11-15 08:52:28 +00:00
rows , err := tx . Query ( `
SELECT
id ,
name ,
color ,
active ,
type ,
timestamp ,
deleted_at_clock_value ,
public_key ,
unviewed_message_count ,
last_clock_value ,
Move to protobuf for Message type (#1706)
* Use a single Message type `v1/message.go` and `message.go` are the same now, and they embed `protobuf.ChatMessage`
* Use `SendChatMessage` for sending chat messages, this is basically the old `Send` but a bit more flexible so we can send different message types (stickers,commands), and not just text.
* Remove dedup from services/shhext. Because now we process in status-protocol, dedup makes less sense, as those messages are going to be processed anyway, so removing for now, we can re-evaluate if bringing it to status-go or not.
* Change the various retrieveX method to a single one:
`RetrieveAll` will be processing those messages that it can process (Currently only `Message`), and return the rest in `RawMessages` (still transit). The format for the response is:
`Chats`: -> The chats updated by receiving the message
`Messages`: -> The messages retrieved (already matched to a chat)
`Contacts`: -> The contacts updated by the messages
`RawMessages` -> Anything else that can't be parsed, eventually as we move everything to status-protocol-go this will go away.
2019-12-05 16:25:34 +00:00
last_message ,
2019-11-15 08:52:28 +00:00
members ,
membership_updates
FROM chats
ORDER BY chats . timestamp DESC
` )
2019-07-30 18:39:16 +00:00
if err != nil {
2019-11-15 08:52:28 +00:00
return
2019-07-30 18:39:16 +00:00
}
defer rows . Close ( )
for rows . Next ( ) {
2019-11-15 08:52:28 +00:00
var (
chat Chat
encodedMembers [ ] byte
encodedMembershipUpdates [ ] byte
pkey [ ] byte
)
err = rows . Scan (
2019-07-30 18:39:16 +00:00
& chat . ID ,
& chat . Name ,
& chat . Color ,
& chat . Active ,
& chat . ChatType ,
& chat . Timestamp ,
& chat . DeletedAtClockValue ,
& pkey ,
& chat . UnviewedMessagesCount ,
& chat . LastClockValue ,
Move to protobuf for Message type (#1706)
* Use a single Message type `v1/message.go` and `message.go` are the same now, and they embed `protobuf.ChatMessage`
* Use `SendChatMessage` for sending chat messages, this is basically the old `Send` but a bit more flexible so we can send different message types (stickers,commands), and not just text.
* Remove dedup from services/shhext. Because now we process in status-protocol, dedup makes less sense, as those messages are going to be processed anyway, so removing for now, we can re-evaluate if bringing it to status-go or not.
* Change the various retrieveX method to a single one:
`RetrieveAll` will be processing those messages that it can process (Currently only `Message`), and return the rest in `RawMessages` (still transit). The format for the response is:
`Chats`: -> The chats updated by receiving the message
`Messages`: -> The messages retrieved (already matched to a chat)
`Contacts`: -> The contacts updated by the messages
`RawMessages` -> Anything else that can't be parsed, eventually as we move everything to status-protocol-go this will go away.
2019-12-05 16:25:34 +00:00
& chat . LastMessage ,
2019-07-30 18:39:16 +00:00
& encodedMembers ,
& encodedMembershipUpdates ,
)
if err != nil {
2019-11-15 08:52:28 +00:00
return
2019-07-30 18:39:16 +00:00
}
// Restore members
membersDecoder := gob . NewDecoder ( bytes . NewBuffer ( encodedMembers ) )
2019-11-15 08:52:28 +00:00
err = membersDecoder . Decode ( & chat . Members )
if err != nil {
return
2019-07-30 18:39:16 +00:00
}
// Restore membership updates
membershipUpdatesDecoder := gob . NewDecoder ( bytes . NewBuffer ( encodedMembershipUpdates ) )
2019-11-15 08:52:28 +00:00
err = membershipUpdatesDecoder . Decode ( & chat . MembershipUpdates )
if err != nil {
return
2019-07-30 18:39:16 +00:00
}
if len ( pkey ) != 0 {
chat . PublicKey , err = crypto . UnmarshalPubkey ( pkey )
if err != nil {
2019-11-15 08:52:28 +00:00
return
2019-07-30 18:39:16 +00:00
}
}
2019-11-15 08:52:28 +00:00
chats = append ( chats , & chat )
2019-07-30 18:39:16 +00:00
}
2019-11-15 08:52:28 +00:00
return
2019-07-30 18:39:16 +00:00
}
func ( db sqlitePersistence ) Contacts ( ) ( [ ] * Contact , error ) {
2019-11-15 08:52:28 +00:00
rows , err := db . db . Query ( `
SELECT
id ,
address ,
name ,
alias ,
identicon ,
photo ,
last_updated ,
system_tags ,
device_info ,
ens_verified ,
ens_verified_at ,
tribute_to_talk
FROM contacts
` )
2019-07-30 18:39:16 +00:00
if err != nil {
return nil , err
}
defer rows . Close ( )
var response [ ] * Contact
for rows . Next ( ) {
2019-11-15 08:52:28 +00:00
var (
contact Contact
encodedDeviceInfo [ ] byte
encodedSystemTags [ ] byte
)
2019-07-30 18:39:16 +00:00
err := rows . Scan (
& contact . ID ,
& contact . Address ,
& contact . Name ,
2019-09-26 07:01:17 +00:00
& contact . Alias ,
& contact . Identicon ,
2019-07-30 18:39:16 +00:00
& contact . Photo ,
& contact . LastUpdated ,
& encodedSystemTags ,
& encodedDeviceInfo ,
2019-11-04 10:08:22 +00:00
& contact . ENSVerified ,
& contact . ENSVerifiedAt ,
2019-07-30 18:39:16 +00:00
& contact . TributeToTalk ,
)
if err != nil {
return nil , err
}
2019-09-26 07:01:17 +00:00
if encodedDeviceInfo != nil {
// Restore device info
deviceInfoDecoder := gob . NewDecoder ( bytes . NewBuffer ( encodedDeviceInfo ) )
if err := deviceInfoDecoder . Decode ( & contact . DeviceInfo ) ; err != nil {
return nil , err
}
2019-07-30 18:39:16 +00:00
}
2019-09-26 07:01:17 +00:00
if encodedSystemTags != nil {
// Restore system tags
systemTagsDecoder := gob . NewDecoder ( bytes . NewBuffer ( encodedSystemTags ) )
if err := systemTagsDecoder . Decode ( & contact . SystemTags ) ; err != nil {
return nil , err
}
2019-07-30 18:39:16 +00:00
}
2019-11-15 08:52:28 +00:00
response = append ( response , & contact )
2019-07-30 18:39:16 +00:00
}
return response , nil
}
Move to protobuf for Message type (#1706)
* Use a single Message type `v1/message.go` and `message.go` are the same now, and they embed `protobuf.ChatMessage`
* Use `SendChatMessage` for sending chat messages, this is basically the old `Send` but a bit more flexible so we can send different message types (stickers,commands), and not just text.
* Remove dedup from services/shhext. Because now we process in status-protocol, dedup makes less sense, as those messages are going to be processed anyway, so removing for now, we can re-evaluate if bringing it to status-go or not.
* Change the various retrieveX method to a single one:
`RetrieveAll` will be processing those messages that it can process (Currently only `Message`), and return the rest in `RawMessages` (still transit). The format for the response is:
`Chats`: -> The chats updated by receiving the message
`Messages`: -> The messages retrieved (already matched to a chat)
`Contacts`: -> The contacts updated by the messages
`RawMessages` -> Anything else that can't be parsed, eventually as we move everything to status-protocol-go this will go away.
2019-12-05 16:25:34 +00:00
func ( db sqlitePersistence ) SetContactsENSData ( contacts [ ] * Contact ) error {
2019-09-26 07:01:17 +00:00
tx , err := db . db . BeginTx ( context . Background ( ) , & sql . TxOptions { } )
if err != nil {
return err
}
defer func ( ) {
if err == nil {
err = tx . Commit ( )
return
}
// don't shadow original error
_ = tx . Rollback ( )
} ( )
2019-11-04 10:08:22 +00:00
// Ensure contacts exists
err = db . SetContactsGeneratedData ( contacts , tx )
if err != nil {
return err
}
// Update ens data
for _ , contact := range contacts {
_ , err := tx . Exec ( ` UPDATE contacts SET name = ?, ens_verified = ? , ens_verified_at = ? WHERE id = ? ` , contact . Name , contact . ENSVerified , contact . ENSVerifiedAt , contact . ID )
if err != nil {
return err
}
}
return nil
}
// SetContactsGeneratedData sets a contact generated data if not existing already
// in the database
Move to protobuf for Message type (#1706)
* Use a single Message type `v1/message.go` and `message.go` are the same now, and they embed `protobuf.ChatMessage`
* Use `SendChatMessage` for sending chat messages, this is basically the old `Send` but a bit more flexible so we can send different message types (stickers,commands), and not just text.
* Remove dedup from services/shhext. Because now we process in status-protocol, dedup makes less sense, as those messages are going to be processed anyway, so removing for now, we can re-evaluate if bringing it to status-go or not.
* Change the various retrieveX method to a single one:
`RetrieveAll` will be processing those messages that it can process (Currently only `Message`), and return the rest in `RawMessages` (still transit). The format for the response is:
`Chats`: -> The chats updated by receiving the message
`Messages`: -> The messages retrieved (already matched to a chat)
`Contacts`: -> The contacts updated by the messages
`RawMessages` -> Anything else that can't be parsed, eventually as we move everything to status-protocol-go this will go away.
2019-12-05 16:25:34 +00:00
func ( db sqlitePersistence ) SetContactsGeneratedData ( contacts [ ] * Contact , tx * sql . Tx ) ( err error ) {
2019-11-04 10:08:22 +00:00
if tx == nil {
tx , err = db . db . BeginTx ( context . Background ( ) , & sql . TxOptions { } )
if err != nil {
return err
}
defer func ( ) {
if err == nil {
err = tx . Commit ( )
return
}
// don't shadow original error
_ = tx . Rollback ( )
} ( )
}
2019-09-26 07:01:17 +00:00
for _ , contact := range contacts {
2019-11-15 08:52:28 +00:00
_ , err = tx . Exec ( `
INSERT OR IGNORE INTO contacts (
id ,
address ,
name ,
alias ,
identicon ,
photo ,
last_updated ,
tribute_to_talk
) VALUES ( ? , ? , "" , ? , ? , "" , 0 , "" ) ` ,
2019-09-26 07:01:17 +00:00
contact . ID ,
contact . Address ,
contact . Alias ,
contact . Identicon ,
)
if err != nil {
2019-11-15 08:52:28 +00:00
return
2019-09-26 07:01:17 +00:00
}
}
2019-11-15 08:52:28 +00:00
return
2019-09-26 07:01:17 +00:00
}
2019-11-15 08:52:28 +00:00
func ( db sqlitePersistence ) SaveContact ( contact Contact , tx * sql . Tx ) ( err error ) {
2019-08-20 11:20:25 +00:00
if tx == nil {
tx , err = db . db . BeginTx ( context . Background ( ) , & sql . TxOptions { } )
if err != nil {
2019-11-15 08:52:28 +00:00
return
2019-08-20 11:20:25 +00:00
}
defer func ( ) {
if err == nil {
err = tx . Commit ( )
return
}
// don't shadow original error
_ = tx . Rollback ( )
} ( )
}
2019-07-30 18:39:16 +00:00
// Encode device info
var encodedDeviceInfo bytes . Buffer
deviceInfoEncoder := gob . NewEncoder ( & encodedDeviceInfo )
2019-11-15 08:52:28 +00:00
err = deviceInfoEncoder . Encode ( contact . DeviceInfo )
if err != nil {
return
2019-07-30 18:39:16 +00:00
}
// Encoded system tags
var encodedSystemTags bytes . Buffer
systemTagsEncoder := gob . NewEncoder ( & encodedSystemTags )
2019-11-15 08:52:28 +00:00
err = systemTagsEncoder . Encode ( contact . SystemTags )
if err != nil {
return
2019-07-30 18:39:16 +00:00
}
// Insert record
2019-11-15 08:52:28 +00:00
stmt , err := tx . Prepare ( `
INSERT INTO contacts (
id ,
address ,
name ,
alias ,
identicon ,
photo ,
last_updated ,
system_tags ,
device_info ,
ens_verified ,
ens_verified_at ,
tribute_to_talk
) VALUES ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? )
` )
2019-07-30 18:39:16 +00:00
if err != nil {
2019-11-15 08:52:28 +00:00
return
2019-07-30 18:39:16 +00:00
}
defer stmt . Close ( )
_ , err = stmt . Exec (
contact . ID ,
contact . Address ,
contact . Name ,
2019-09-26 07:01:17 +00:00
contact . Alias ,
contact . Identicon ,
2019-07-30 18:39:16 +00:00
contact . Photo ,
contact . LastUpdated ,
encodedSystemTags . Bytes ( ) ,
encodedDeviceInfo . Bytes ( ) ,
2019-11-04 10:08:22 +00:00
contact . ENSVerified ,
contact . ENSVerifiedAt ,
2019-07-30 18:39:16 +00:00
contact . TributeToTalk ,
)
2019-11-15 08:52:28 +00:00
return
2019-07-30 18:39:16 +00:00
}