status-go/protocol/protobuf/chat_message.pb.go

1821 lines
65 KiB
Go
Raw Normal View History

// Code generated by protoc-gen-go. DO NOT EDIT.
// source: chat_message.proto
package protobuf
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
math "math"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
type AudioMessage_AudioType int32
const (
AudioMessage_UNKNOWN_AUDIO_TYPE AudioMessage_AudioType = 0
AudioMessage_AAC AudioMessage_AudioType = 1
AudioMessage_AMR AudioMessage_AudioType = 2
)
var AudioMessage_AudioType_name = map[int32]string{
0: "UNKNOWN_AUDIO_TYPE",
1: "AAC",
2: "AMR",
}
var AudioMessage_AudioType_value = map[string]int32{
"UNKNOWN_AUDIO_TYPE": 0,
"AAC": 1,
"AMR": 2,
}
func (x AudioMessage_AudioType) String() string {
return proto.EnumName(AudioMessage_AudioType_name, int32(x))
}
func (AudioMessage_AudioType) EnumDescriptor() ([]byte, []int) {
return fileDescriptor_263952f55fd35689, []int{2, 0}
}
type UnfurledLink_LinkType int32
const (
UnfurledLink_LINK UnfurledLink_LinkType = 0
UnfurledLink_IMAGE UnfurledLink_LinkType = 1
)
var UnfurledLink_LinkType_name = map[int32]string{
0: "LINK",
1: "IMAGE",
}
var UnfurledLink_LinkType_value = map[string]int32{
"LINK": 0,
"IMAGE": 1,
}
func (x UnfurledLink_LinkType) String() string {
return proto.EnumName(UnfurledLink_LinkType_name, int32(x))
}
func (UnfurledLink_LinkType) EnumDescriptor() ([]byte, []int) {
return fileDescriptor_263952f55fd35689, []int{11, 0}
}
type ChatMessage_ContentType int32
const (
ChatMessage_UNKNOWN_CONTENT_TYPE ChatMessage_ContentType = 0
ChatMessage_TEXT_PLAIN ChatMessage_ContentType = 1
ChatMessage_STICKER ChatMessage_ContentType = 2
ChatMessage_STATUS ChatMessage_ContentType = 3
ChatMessage_EMOJI ChatMessage_ContentType = 4
ChatMessage_TRANSACTION_COMMAND ChatMessage_ContentType = 5
2020-01-28 11:16:28 +00:00
// Only local
ChatMessage_SYSTEM_MESSAGE_CONTENT_PRIVATE_GROUP ChatMessage_ContentType = 6
ChatMessage_IMAGE ChatMessage_ContentType = 7
ChatMessage_AUDIO ChatMessage_ContentType = 8
ChatMessage_COMMUNITY ChatMessage_ContentType = 9
2021-03-25 15:15:22 +00:00
// Only local
ChatMessage_SYSTEM_MESSAGE_GAP ChatMessage_ContentType = 10
ChatMessage_CONTACT_REQUEST ChatMessage_ContentType = 11
ChatMessage_DISCORD_MESSAGE ChatMessage_ContentType = 12
ChatMessage_IDENTITY_VERIFICATION ChatMessage_ContentType = 13
// Only local
ChatMessage_SYSTEM_MESSAGE_PINNED_MESSAGE ChatMessage_ContentType = 14
// Only local
ChatMessage_SYSTEM_MESSAGE_MUTUAL_EVENT_SENT ChatMessage_ContentType = 15
// Only local
ChatMessage_SYSTEM_MESSAGE_MUTUAL_EVENT_ACCEPTED ChatMessage_ContentType = 16
// Only local
ChatMessage_SYSTEM_MESSAGE_MUTUAL_EVENT_REMOVED ChatMessage_ContentType = 17
)
var ChatMessage_ContentType_name = map[int32]string{
0: "UNKNOWN_CONTENT_TYPE",
1: "TEXT_PLAIN",
2: "STICKER",
3: "STATUS",
4: "EMOJI",
5: "TRANSACTION_COMMAND",
6: "SYSTEM_MESSAGE_CONTENT_PRIVATE_GROUP",
7: "IMAGE",
8: "AUDIO",
9: "COMMUNITY",
10: "SYSTEM_MESSAGE_GAP",
11: "CONTACT_REQUEST",
12: "DISCORD_MESSAGE",
13: "IDENTITY_VERIFICATION",
14: "SYSTEM_MESSAGE_PINNED_MESSAGE",
15: "SYSTEM_MESSAGE_MUTUAL_EVENT_SENT",
16: "SYSTEM_MESSAGE_MUTUAL_EVENT_ACCEPTED",
17: "SYSTEM_MESSAGE_MUTUAL_EVENT_REMOVED",
}
var ChatMessage_ContentType_value = map[string]int32{
"UNKNOWN_CONTENT_TYPE": 0,
"TEXT_PLAIN": 1,
"STICKER": 2,
"STATUS": 3,
"EMOJI": 4,
"TRANSACTION_COMMAND": 5,
"SYSTEM_MESSAGE_CONTENT_PRIVATE_GROUP": 6,
"IMAGE": 7,
"AUDIO": 8,
"COMMUNITY": 9,
"SYSTEM_MESSAGE_GAP": 10,
"CONTACT_REQUEST": 11,
"DISCORD_MESSAGE": 12,
"IDENTITY_VERIFICATION": 13,
"SYSTEM_MESSAGE_PINNED_MESSAGE": 14,
"SYSTEM_MESSAGE_MUTUAL_EVENT_SENT": 15,
"SYSTEM_MESSAGE_MUTUAL_EVENT_ACCEPTED": 16,
"SYSTEM_MESSAGE_MUTUAL_EVENT_REMOVED": 17,
}
func (x ChatMessage_ContentType) String() string {
return proto.EnumName(ChatMessage_ContentType_name, int32(x))
}
func (ChatMessage_ContentType) EnumDescriptor() ([]byte, []int) {
return fileDescriptor_263952f55fd35689, []int{17, 0}
}
type StickerMessage struct {
Hash string `protobuf:"bytes,1,opt,name=hash,proto3" json:"hash,omitempty"`
Pack int32 `protobuf:"varint,2,opt,name=pack,proto3" json:"pack,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *StickerMessage) Reset() { *m = StickerMessage{} }
func (m *StickerMessage) String() string { return proto.CompactTextString(m) }
func (*StickerMessage) ProtoMessage() {}
func (*StickerMessage) Descriptor() ([]byte, []int) {
return fileDescriptor_263952f55fd35689, []int{0}
}
func (m *StickerMessage) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_StickerMessage.Unmarshal(m, b)
}
func (m *StickerMessage) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_StickerMessage.Marshal(b, m, deterministic)
}
func (m *StickerMessage) XXX_Merge(src proto.Message) {
xxx_messageInfo_StickerMessage.Merge(m, src)
}
func (m *StickerMessage) XXX_Size() int {
return xxx_messageInfo_StickerMessage.Size(m)
}
func (m *StickerMessage) XXX_DiscardUnknown() {
xxx_messageInfo_StickerMessage.DiscardUnknown(m)
}
var xxx_messageInfo_StickerMessage proto.InternalMessageInfo
func (m *StickerMessage) GetHash() string {
if m != nil {
return m.Hash
}
return ""
}
func (m *StickerMessage) GetPack() int32 {
if m != nil {
return m.Pack
}
return 0
}
type ImageMessage struct {
Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"`
Type ImageType `protobuf:"varint,2,opt,name=type,proto3,enum=protobuf.ImageType" json:"type,omitempty"`
AlbumId string `protobuf:"bytes,3,opt,name=album_id,json=albumId,proto3" json:"album_id,omitempty"`
Width uint32 `protobuf:"varint,4,opt,name=width,proto3" json:"width,omitempty"`
Height uint32 `protobuf:"varint,5,opt,name=height,proto3" json:"height,omitempty"`
AlbumImagesCount uint32 `protobuf:"varint,6,opt,name=album_images_count,json=albumImagesCount,proto3" json:"album_images_count,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ImageMessage) Reset() { *m = ImageMessage{} }
func (m *ImageMessage) String() string { return proto.CompactTextString(m) }
func (*ImageMessage) ProtoMessage() {}
func (*ImageMessage) Descriptor() ([]byte, []int) {
return fileDescriptor_263952f55fd35689, []int{1}
}
func (m *ImageMessage) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ImageMessage.Unmarshal(m, b)
}
func (m *ImageMessage) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ImageMessage.Marshal(b, m, deterministic)
}
func (m *ImageMessage) XXX_Merge(src proto.Message) {
xxx_messageInfo_ImageMessage.Merge(m, src)
}
func (m *ImageMessage) XXX_Size() int {
return xxx_messageInfo_ImageMessage.Size(m)
}
func (m *ImageMessage) XXX_DiscardUnknown() {
xxx_messageInfo_ImageMessage.DiscardUnknown(m)
}
var xxx_messageInfo_ImageMessage proto.InternalMessageInfo
func (m *ImageMessage) GetPayload() []byte {
if m != nil {
return m.Payload
}
return nil
}
func (m *ImageMessage) GetType() ImageType {
if m != nil {
return m.Type
}
return ImageType_UNKNOWN_IMAGE_TYPE
}
func (m *ImageMessage) GetAlbumId() string {
if m != nil {
return m.AlbumId
}
return ""
}
func (m *ImageMessage) GetWidth() uint32 {
if m != nil {
return m.Width
}
return 0
}
func (m *ImageMessage) GetHeight() uint32 {
if m != nil {
return m.Height
}
return 0
}
func (m *ImageMessage) GetAlbumImagesCount() uint32 {
if m != nil {
return m.AlbumImagesCount
}
return 0
}
type AudioMessage struct {
Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"`
Type AudioMessage_AudioType `protobuf:"varint,2,opt,name=type,proto3,enum=protobuf.AudioMessage_AudioType" json:"type,omitempty"`
DurationMs uint64 `protobuf:"varint,3,opt,name=duration_ms,json=durationMs,proto3" json:"duration_ms,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *AudioMessage) Reset() { *m = AudioMessage{} }
func (m *AudioMessage) String() string { return proto.CompactTextString(m) }
func (*AudioMessage) ProtoMessage() {}
func (*AudioMessage) Descriptor() ([]byte, []int) {
return fileDescriptor_263952f55fd35689, []int{2}
}
func (m *AudioMessage) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_AudioMessage.Unmarshal(m, b)
}
func (m *AudioMessage) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_AudioMessage.Marshal(b, m, deterministic)
}
func (m *AudioMessage) XXX_Merge(src proto.Message) {
xxx_messageInfo_AudioMessage.Merge(m, src)
}
func (m *AudioMessage) XXX_Size() int {
return xxx_messageInfo_AudioMessage.Size(m)
}
func (m *AudioMessage) XXX_DiscardUnknown() {
xxx_messageInfo_AudioMessage.DiscardUnknown(m)
}
var xxx_messageInfo_AudioMessage proto.InternalMessageInfo
func (m *AudioMessage) GetPayload() []byte {
if m != nil {
return m.Payload
}
return nil
}
func (m *AudioMessage) GetType() AudioMessage_AudioType {
if m != nil {
return m.Type
}
return AudioMessage_UNKNOWN_AUDIO_TYPE
}
func (m *AudioMessage) GetDurationMs() uint64 {
if m != nil {
return m.DurationMs
2020-06-23 14:30:39 +00:00
}
return 0
}
2021-06-07 08:31:27 +00:00
type EditMessage struct {
Clock uint64 `protobuf:"varint,1,opt,name=clock,proto3" json:"clock,omitempty"`
// Text of the message
Text string `protobuf:"bytes,2,opt,name=text,proto3" json:"text,omitempty"`
ChatId string `protobuf:"bytes,3,opt,name=chat_id,json=chatId,proto3" json:"chat_id,omitempty"`
MessageId string `protobuf:"bytes,4,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"`
// Grant for community edit messages
Grant []byte `protobuf:"bytes,5,opt,name=grant,proto3" json:"grant,omitempty"`
// The type of message (public/one-to-one/private-group-chat)
MessageType MessageType `protobuf:"varint,6,opt,name=message_type,json=messageType,proto3,enum=protobuf.MessageType" json:"message_type,omitempty"`
ContentType ChatMessage_ContentType `protobuf:"varint,7,opt,name=content_type,json=contentType,proto3,enum=protobuf.ChatMessage_ContentType" json:"content_type,omitempty"`
UnfurledLinks []*UnfurledLink `protobuf:"bytes,8,rep,name=unfurled_links,json=unfurledLinks,proto3" json:"unfurled_links,omitempty"`
UnfurledStatusLinks *UnfurledStatusLinks `protobuf:"bytes,9,opt,name=unfurled_status_links,json=unfurledStatusLinks,proto3" json:"unfurled_status_links,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *EditMessage) Reset() { *m = EditMessage{} }
func (m *EditMessage) String() string { return proto.CompactTextString(m) }
func (*EditMessage) ProtoMessage() {}
func (*EditMessage) Descriptor() ([]byte, []int) {
return fileDescriptor_263952f55fd35689, []int{3}
2021-06-07 08:31:27 +00:00
}
func (m *EditMessage) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_EditMessage.Unmarshal(m, b)
}
func (m *EditMessage) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_EditMessage.Marshal(b, m, deterministic)
}
func (m *EditMessage) XXX_Merge(src proto.Message) {
xxx_messageInfo_EditMessage.Merge(m, src)
}
func (m *EditMessage) XXX_Size() int {
return xxx_messageInfo_EditMessage.Size(m)
}
func (m *EditMessage) XXX_DiscardUnknown() {
xxx_messageInfo_EditMessage.DiscardUnknown(m)
}
var xxx_messageInfo_EditMessage proto.InternalMessageInfo
func (m *EditMessage) GetClock() uint64 {
if m != nil {
return m.Clock
2021-06-07 08:31:27 +00:00
}
return 0
}
func (m *EditMessage) GetText() string {
if m != nil {
return m.Text
2021-06-07 08:31:27 +00:00
}
return ""
}
func (m *EditMessage) GetChatId() string {
if m != nil {
return m.ChatId
2021-06-07 08:31:27 +00:00
}
return ""
}
func (m *EditMessage) GetMessageId() string {
if m != nil {
return m.MessageId
2021-06-07 08:31:27 +00:00
}
return ""
}
func (m *EditMessage) GetGrant() []byte {
if m != nil {
return m.Grant
2021-06-07 08:31:27 +00:00
}
return nil
}
func (m *EditMessage) GetMessageType() MessageType {
if m != nil {
return m.MessageType
2021-06-07 08:31:27 +00:00
}
return MessageType_UNKNOWN_MESSAGE_TYPE
}
func (m *EditMessage) GetContentType() ChatMessage_ContentType {
if m != nil {
return m.ContentType
}
return ChatMessage_UNKNOWN_CONTENT_TYPE
}
func (m *EditMessage) GetUnfurledLinks() []*UnfurledLink {
if m != nil {
return m.UnfurledLinks
}
return nil
}
func (m *EditMessage) GetUnfurledStatusLinks() *UnfurledStatusLinks {
if m != nil {
return m.UnfurledStatusLinks
2023-10-13 12:25:34 +00:00
}
return nil
}
type DeleteMessage struct {
Clock uint64 `protobuf:"varint,1,opt,name=clock,proto3" json:"clock,omitempty"`
ChatId string `protobuf:"bytes,2,opt,name=chat_id,json=chatId,proto3" json:"chat_id,omitempty"`
MessageId string `protobuf:"bytes,3,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"`
// Grant for community delete messages
Grant []byte `protobuf:"bytes,4,opt,name=grant,proto3" json:"grant,omitempty"`
// The type of message (public/one-to-one/private-group-chat)
MessageType MessageType `protobuf:"varint,5,opt,name=message_type,json=messageType,proto3,enum=protobuf.MessageType" json:"message_type,omitempty"`
DeletedBy string `protobuf:"bytes,6,opt,name=deleted_by,json=deletedBy,proto3" json:"deleted_by,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *DeleteMessage) Reset() { *m = DeleteMessage{} }
func (m *DeleteMessage) String() string { return proto.CompactTextString(m) }
func (*DeleteMessage) ProtoMessage() {}
func (*DeleteMessage) Descriptor() ([]byte, []int) {
return fileDescriptor_263952f55fd35689, []int{4}
}
func (m *DeleteMessage) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_DeleteMessage.Unmarshal(m, b)
}
func (m *DeleteMessage) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_DeleteMessage.Marshal(b, m, deterministic)
}
func (m *DeleteMessage) XXX_Merge(src proto.Message) {
xxx_messageInfo_DeleteMessage.Merge(m, src)
}
func (m *DeleteMessage) XXX_Size() int {
return xxx_messageInfo_DeleteMessage.Size(m)
}
func (m *DeleteMessage) XXX_DiscardUnknown() {
xxx_messageInfo_DeleteMessage.DiscardUnknown(m)
}
var xxx_messageInfo_DeleteMessage proto.InternalMessageInfo
func (m *DeleteMessage) GetClock() uint64 {
if m != nil {
return m.Clock
}
return 0
}
func (m *DeleteMessage) GetChatId() string {
if m != nil {
return m.ChatId
}
return ""
}
func (m *DeleteMessage) GetMessageId() string {
if m != nil {
return m.MessageId
}
return ""
}
func (m *DeleteMessage) GetGrant() []byte {
if m != nil {
return m.Grant
}
return nil
}
func (m *DeleteMessage) GetMessageType() MessageType {
if m != nil {
return m.MessageType
}
return MessageType_UNKNOWN_MESSAGE_TYPE
}
func (m *DeleteMessage) GetDeletedBy() string {
if m != nil {
return m.DeletedBy
}
return ""
}
type SyncDeleteForMeMessage struct {
Clock uint64 `protobuf:"varint,1,opt,name=clock,proto3" json:"clock,omitempty"`
MessageId string `protobuf:"bytes,2,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *SyncDeleteForMeMessage) Reset() { *m = SyncDeleteForMeMessage{} }
func (m *SyncDeleteForMeMessage) String() string { return proto.CompactTextString(m) }
func (*SyncDeleteForMeMessage) ProtoMessage() {}
func (*SyncDeleteForMeMessage) Descriptor() ([]byte, []int) {
return fileDescriptor_263952f55fd35689, []int{5}
}
func (m *SyncDeleteForMeMessage) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_SyncDeleteForMeMessage.Unmarshal(m, b)
}
func (m *SyncDeleteForMeMessage) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_SyncDeleteForMeMessage.Marshal(b, m, deterministic)
}
func (m *SyncDeleteForMeMessage) XXX_Merge(src proto.Message) {
xxx_messageInfo_SyncDeleteForMeMessage.Merge(m, src)
}
func (m *SyncDeleteForMeMessage) XXX_Size() int {
return xxx_messageInfo_SyncDeleteForMeMessage.Size(m)
}
func (m *SyncDeleteForMeMessage) XXX_DiscardUnknown() {
xxx_messageInfo_SyncDeleteForMeMessage.DiscardUnknown(m)
}
var xxx_messageInfo_SyncDeleteForMeMessage proto.InternalMessageInfo
func (m *SyncDeleteForMeMessage) GetClock() uint64 {
if m != nil {
return m.Clock
2022-09-28 11:42:17 +00:00
}
return 0
}
func (m *SyncDeleteForMeMessage) GetMessageId() string {
if m != nil {
return m.MessageId
2022-09-28 11:42:17 +00:00
}
return ""
}
feat: introduce messenger APIs to extract discord channels As part of the new Discord <-> Status Community Import functionality, we're adding an API that extracts all discord categories and channels from a previously exported discord export file. These APIs can be used in clients to show the user what categories and channels will be imported later on. There are two APIs: 1. `Messenger.ExtractDiscordCategoriesAndChannels(filesToimport []string) (*MessengerResponse, map[string]*discord.ImportError)` This takes a list of exported discord export (JSON) files (typically one per channel), reads them, and extracts the categories and channels into dedicated data structures (`[]DiscordChannel` and `[]DiscordCategory`) It also returns the oldest message timestamp found in all extracted channels. The API is synchronous and returns the extracted data as a `*MessengerResponse`. This allows to make the API available status-go's RPC interface. The error case is a `map[string]*discord.ImportError` where each key is a file path of a JSON file that we tried to extract data from, and the value a `discord.ImportError` which holds an error message and an error code, allowing for distinguishing between "critical" errors and "non-critical" errors. 2. `Messenger.RequestExtractDiscordCategoriesAndChannels(filesToImport []string)` This is the asynchronous counterpart to `ExtractDiscordCategoriesAndChannels`. The reason this API has been added is because discord servers can have a lot of message and channel data, which causes `ExtractDiscordCategoriesAndChannels` to block the thread for too long, making apps potentially feel like they are stuck. This API runs inside a go routine, eventually calls `ExtractDiscordCategoriesAndChannels`, and then emits a newly introduced `DiscordCategoriesAndChannelsExtractedSignal` that clients can react to. Failure of extraction has to be determined by the `discord.ImportErrors` emitted by the signal. **A note about exported discord history files** We expect users to export their discord histories via the [DiscordChatExporter](https://github.com/Tyrrrz/DiscordChatExporter/wiki/GUI%2C-CLI-and-Formats-explained#exportguild) tool. The tool allows to export the data in different formats, such as JSON, HTML and CSV. We expect users to have their data exported as JSON. Closes: https://github.com/status-im/status-desktop/issues/6690
2022-07-13 09:33:53 +00:00
type DiscordMessage struct {
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"`
Timestamp string `protobuf:"bytes,3,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
TimestampEdited string `protobuf:"bytes,4,opt,name=timestampEdited,proto3" json:"timestampEdited,omitempty"`
Content string `protobuf:"bytes,5,opt,name=content,proto3" json:"content,omitempty"`
Author *DiscordMessageAuthor `protobuf:"bytes,6,opt,name=author,proto3" json:"author,omitempty"`
Reference *DiscordMessageReference `protobuf:"bytes,7,opt,name=reference,proto3" json:"reference,omitempty"`
Attachments []*DiscordMessageAttachment `protobuf:"bytes,8,rep,name=attachments,proto3" json:"attachments,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *DiscordMessage) Reset() { *m = DiscordMessage{} }
func (m *DiscordMessage) String() string { return proto.CompactTextString(m) }
func (*DiscordMessage) ProtoMessage() {}
func (*DiscordMessage) Descriptor() ([]byte, []int) {
return fileDescriptor_263952f55fd35689, []int{6}
}
func (m *DiscordMessage) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_DiscordMessage.Unmarshal(m, b)
}
func (m *DiscordMessage) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_DiscordMessage.Marshal(b, m, deterministic)
}
func (m *DiscordMessage) XXX_Merge(src proto.Message) {
xxx_messageInfo_DiscordMessage.Merge(m, src)
}
func (m *DiscordMessage) XXX_Size() int {
return xxx_messageInfo_DiscordMessage.Size(m)
}
func (m *DiscordMessage) XXX_DiscardUnknown() {
xxx_messageInfo_DiscordMessage.DiscardUnknown(m)
}
var xxx_messageInfo_DiscordMessage proto.InternalMessageInfo
func (m *DiscordMessage) GetId() string {
if m != nil {
return m.Id
feat: introduce messenger APIs to extract discord channels As part of the new Discord <-> Status Community Import functionality, we're adding an API that extracts all discord categories and channels from a previously exported discord export file. These APIs can be used in clients to show the user what categories and channels will be imported later on. There are two APIs: 1. `Messenger.ExtractDiscordCategoriesAndChannels(filesToimport []string) (*MessengerResponse, map[string]*discord.ImportError)` This takes a list of exported discord export (JSON) files (typically one per channel), reads them, and extracts the categories and channels into dedicated data structures (`[]DiscordChannel` and `[]DiscordCategory`) It also returns the oldest message timestamp found in all extracted channels. The API is synchronous and returns the extracted data as a `*MessengerResponse`. This allows to make the API available status-go's RPC interface. The error case is a `map[string]*discord.ImportError` where each key is a file path of a JSON file that we tried to extract data from, and the value a `discord.ImportError` which holds an error message and an error code, allowing for distinguishing between "critical" errors and "non-critical" errors. 2. `Messenger.RequestExtractDiscordCategoriesAndChannels(filesToImport []string)` This is the asynchronous counterpart to `ExtractDiscordCategoriesAndChannels`. The reason this API has been added is because discord servers can have a lot of message and channel data, which causes `ExtractDiscordCategoriesAndChannels` to block the thread for too long, making apps potentially feel like they are stuck. This API runs inside a go routine, eventually calls `ExtractDiscordCategoriesAndChannels`, and then emits a newly introduced `DiscordCategoriesAndChannelsExtractedSignal` that clients can react to. Failure of extraction has to be determined by the `discord.ImportErrors` emitted by the signal. **A note about exported discord history files** We expect users to export their discord histories via the [DiscordChatExporter](https://github.com/Tyrrrz/DiscordChatExporter/wiki/GUI%2C-CLI-and-Formats-explained#exportguild) tool. The tool allows to export the data in different formats, such as JSON, HTML and CSV. We expect users to have their data exported as JSON. Closes: https://github.com/status-im/status-desktop/issues/6690
2022-07-13 09:33:53 +00:00
}
return ""
}
func (m *DiscordMessage) GetType() string {
if m != nil {
return m.Type
feat: introduce messenger APIs to extract discord channels As part of the new Discord <-> Status Community Import functionality, we're adding an API that extracts all discord categories and channels from a previously exported discord export file. These APIs can be used in clients to show the user what categories and channels will be imported later on. There are two APIs: 1. `Messenger.ExtractDiscordCategoriesAndChannels(filesToimport []string) (*MessengerResponse, map[string]*discord.ImportError)` This takes a list of exported discord export (JSON) files (typically one per channel), reads them, and extracts the categories and channels into dedicated data structures (`[]DiscordChannel` and `[]DiscordCategory`) It also returns the oldest message timestamp found in all extracted channels. The API is synchronous and returns the extracted data as a `*MessengerResponse`. This allows to make the API available status-go's RPC interface. The error case is a `map[string]*discord.ImportError` where each key is a file path of a JSON file that we tried to extract data from, and the value a `discord.ImportError` which holds an error message and an error code, allowing for distinguishing between "critical" errors and "non-critical" errors. 2. `Messenger.RequestExtractDiscordCategoriesAndChannels(filesToImport []string)` This is the asynchronous counterpart to `ExtractDiscordCategoriesAndChannels`. The reason this API has been added is because discord servers can have a lot of message and channel data, which causes `ExtractDiscordCategoriesAndChannels` to block the thread for too long, making apps potentially feel like they are stuck. This API runs inside a go routine, eventually calls `ExtractDiscordCategoriesAndChannels`, and then emits a newly introduced `DiscordCategoriesAndChannelsExtractedSignal` that clients can react to. Failure of extraction has to be determined by the `discord.ImportErrors` emitted by the signal. **A note about exported discord history files** We expect users to export their discord histories via the [DiscordChatExporter](https://github.com/Tyrrrz/DiscordChatExporter/wiki/GUI%2C-CLI-and-Formats-explained#exportguild) tool. The tool allows to export the data in different formats, such as JSON, HTML and CSV. We expect users to have their data exported as JSON. Closes: https://github.com/status-im/status-desktop/issues/6690
2022-07-13 09:33:53 +00:00
}
return ""
}
func (m *DiscordMessage) GetTimestamp() string {
if m != nil {
return m.Timestamp
feat: introduce messenger APIs to extract discord channels As part of the new Discord <-> Status Community Import functionality, we're adding an API that extracts all discord categories and channels from a previously exported discord export file. These APIs can be used in clients to show the user what categories and channels will be imported later on. There are two APIs: 1. `Messenger.ExtractDiscordCategoriesAndChannels(filesToimport []string) (*MessengerResponse, map[string]*discord.ImportError)` This takes a list of exported discord export (JSON) files (typically one per channel), reads them, and extracts the categories and channels into dedicated data structures (`[]DiscordChannel` and `[]DiscordCategory`) It also returns the oldest message timestamp found in all extracted channels. The API is synchronous and returns the extracted data as a `*MessengerResponse`. This allows to make the API available status-go's RPC interface. The error case is a `map[string]*discord.ImportError` where each key is a file path of a JSON file that we tried to extract data from, and the value a `discord.ImportError` which holds an error message and an error code, allowing for distinguishing between "critical" errors and "non-critical" errors. 2. `Messenger.RequestExtractDiscordCategoriesAndChannels(filesToImport []string)` This is the asynchronous counterpart to `ExtractDiscordCategoriesAndChannels`. The reason this API has been added is because discord servers can have a lot of message and channel data, which causes `ExtractDiscordCategoriesAndChannels` to block the thread for too long, making apps potentially feel like they are stuck. This API runs inside a go routine, eventually calls `ExtractDiscordCategoriesAndChannels`, and then emits a newly introduced `DiscordCategoriesAndChannelsExtractedSignal` that clients can react to. Failure of extraction has to be determined by the `discord.ImportErrors` emitted by the signal. **A note about exported discord history files** We expect users to export their discord histories via the [DiscordChatExporter](https://github.com/Tyrrrz/DiscordChatExporter/wiki/GUI%2C-CLI-and-Formats-explained#exportguild) tool. The tool allows to export the data in different formats, such as JSON, HTML and CSV. We expect users to have their data exported as JSON. Closes: https://github.com/status-im/status-desktop/issues/6690
2022-07-13 09:33:53 +00:00
}
return ""
}
func (m *DiscordMessage) GetTimestampEdited() string {
if m != nil {
return m.TimestampEdited
feat: introduce messenger APIs to extract discord channels As part of the new Discord <-> Status Community Import functionality, we're adding an API that extracts all discord categories and channels from a previously exported discord export file. These APIs can be used in clients to show the user what categories and channels will be imported later on. There are two APIs: 1. `Messenger.ExtractDiscordCategoriesAndChannels(filesToimport []string) (*MessengerResponse, map[string]*discord.ImportError)` This takes a list of exported discord export (JSON) files (typically one per channel), reads them, and extracts the categories and channels into dedicated data structures (`[]DiscordChannel` and `[]DiscordCategory`) It also returns the oldest message timestamp found in all extracted channels. The API is synchronous and returns the extracted data as a `*MessengerResponse`. This allows to make the API available status-go's RPC interface. The error case is a `map[string]*discord.ImportError` where each key is a file path of a JSON file that we tried to extract data from, and the value a `discord.ImportError` which holds an error message and an error code, allowing for distinguishing between "critical" errors and "non-critical" errors. 2. `Messenger.RequestExtractDiscordCategoriesAndChannels(filesToImport []string)` This is the asynchronous counterpart to `ExtractDiscordCategoriesAndChannels`. The reason this API has been added is because discord servers can have a lot of message and channel data, which causes `ExtractDiscordCategoriesAndChannels` to block the thread for too long, making apps potentially feel like they are stuck. This API runs inside a go routine, eventually calls `ExtractDiscordCategoriesAndChannels`, and then emits a newly introduced `DiscordCategoriesAndChannelsExtractedSignal` that clients can react to. Failure of extraction has to be determined by the `discord.ImportErrors` emitted by the signal. **A note about exported discord history files** We expect users to export their discord histories via the [DiscordChatExporter](https://github.com/Tyrrrz/DiscordChatExporter/wiki/GUI%2C-CLI-and-Formats-explained#exportguild) tool. The tool allows to export the data in different formats, such as JSON, HTML and CSV. We expect users to have their data exported as JSON. Closes: https://github.com/status-im/status-desktop/issues/6690
2022-07-13 09:33:53 +00:00
}
return ""
}
func (m *DiscordMessage) GetContent() string {
if m != nil {
return m.Content
feat: introduce messenger APIs to extract discord channels As part of the new Discord <-> Status Community Import functionality, we're adding an API that extracts all discord categories and channels from a previously exported discord export file. These APIs can be used in clients to show the user what categories and channels will be imported later on. There are two APIs: 1. `Messenger.ExtractDiscordCategoriesAndChannels(filesToimport []string) (*MessengerResponse, map[string]*discord.ImportError)` This takes a list of exported discord export (JSON) files (typically one per channel), reads them, and extracts the categories and channels into dedicated data structures (`[]DiscordChannel` and `[]DiscordCategory`) It also returns the oldest message timestamp found in all extracted channels. The API is synchronous and returns the extracted data as a `*MessengerResponse`. This allows to make the API available status-go's RPC interface. The error case is a `map[string]*discord.ImportError` where each key is a file path of a JSON file that we tried to extract data from, and the value a `discord.ImportError` which holds an error message and an error code, allowing for distinguishing between "critical" errors and "non-critical" errors. 2. `Messenger.RequestExtractDiscordCategoriesAndChannels(filesToImport []string)` This is the asynchronous counterpart to `ExtractDiscordCategoriesAndChannels`. The reason this API has been added is because discord servers can have a lot of message and channel data, which causes `ExtractDiscordCategoriesAndChannels` to block the thread for too long, making apps potentially feel like they are stuck. This API runs inside a go routine, eventually calls `ExtractDiscordCategoriesAndChannels`, and then emits a newly introduced `DiscordCategoriesAndChannelsExtractedSignal` that clients can react to. Failure of extraction has to be determined by the `discord.ImportErrors` emitted by the signal. **A note about exported discord history files** We expect users to export their discord histories via the [DiscordChatExporter](https://github.com/Tyrrrz/DiscordChatExporter/wiki/GUI%2C-CLI-and-Formats-explained#exportguild) tool. The tool allows to export the data in different formats, such as JSON, HTML and CSV. We expect users to have their data exported as JSON. Closes: https://github.com/status-im/status-desktop/issues/6690
2022-07-13 09:33:53 +00:00
}
return ""
}
func (m *DiscordMessage) GetAuthor() *DiscordMessageAuthor {
if m != nil {
return m.Author
feat: introduce messenger APIs to extract discord channels As part of the new Discord <-> Status Community Import functionality, we're adding an API that extracts all discord categories and channels from a previously exported discord export file. These APIs can be used in clients to show the user what categories and channels will be imported later on. There are two APIs: 1. `Messenger.ExtractDiscordCategoriesAndChannels(filesToimport []string) (*MessengerResponse, map[string]*discord.ImportError)` This takes a list of exported discord export (JSON) files (typically one per channel), reads them, and extracts the categories and channels into dedicated data structures (`[]DiscordChannel` and `[]DiscordCategory`) It also returns the oldest message timestamp found in all extracted channels. The API is synchronous and returns the extracted data as a `*MessengerResponse`. This allows to make the API available status-go's RPC interface. The error case is a `map[string]*discord.ImportError` where each key is a file path of a JSON file that we tried to extract data from, and the value a `discord.ImportError` which holds an error message and an error code, allowing for distinguishing between "critical" errors and "non-critical" errors. 2. `Messenger.RequestExtractDiscordCategoriesAndChannels(filesToImport []string)` This is the asynchronous counterpart to `ExtractDiscordCategoriesAndChannels`. The reason this API has been added is because discord servers can have a lot of message and channel data, which causes `ExtractDiscordCategoriesAndChannels` to block the thread for too long, making apps potentially feel like they are stuck. This API runs inside a go routine, eventually calls `ExtractDiscordCategoriesAndChannels`, and then emits a newly introduced `DiscordCategoriesAndChannelsExtractedSignal` that clients can react to. Failure of extraction has to be determined by the `discord.ImportErrors` emitted by the signal. **A note about exported discord history files** We expect users to export their discord histories via the [DiscordChatExporter](https://github.com/Tyrrrz/DiscordChatExporter/wiki/GUI%2C-CLI-and-Formats-explained#exportguild) tool. The tool allows to export the data in different formats, such as JSON, HTML and CSV. We expect users to have their data exported as JSON. Closes: https://github.com/status-im/status-desktop/issues/6690
2022-07-13 09:33:53 +00:00
}
return nil
}
func (m *DiscordMessage) GetReference() *DiscordMessageReference {
if m != nil {
return m.Reference
}
return nil
}
func (m *DiscordMessage) GetAttachments() []*DiscordMessageAttachment {
if m != nil {
return m.Attachments
}
return nil
}
feat: introduce messenger APIs to extract discord channels As part of the new Discord <-> Status Community Import functionality, we're adding an API that extracts all discord categories and channels from a previously exported discord export file. These APIs can be used in clients to show the user what categories and channels will be imported later on. There are two APIs: 1. `Messenger.ExtractDiscordCategoriesAndChannels(filesToimport []string) (*MessengerResponse, map[string]*discord.ImportError)` This takes a list of exported discord export (JSON) files (typically one per channel), reads them, and extracts the categories and channels into dedicated data structures (`[]DiscordChannel` and `[]DiscordCategory`) It also returns the oldest message timestamp found in all extracted channels. The API is synchronous and returns the extracted data as a `*MessengerResponse`. This allows to make the API available status-go's RPC interface. The error case is a `map[string]*discord.ImportError` where each key is a file path of a JSON file that we tried to extract data from, and the value a `discord.ImportError` which holds an error message and an error code, allowing for distinguishing between "critical" errors and "non-critical" errors. 2. `Messenger.RequestExtractDiscordCategoriesAndChannels(filesToImport []string)` This is the asynchronous counterpart to `ExtractDiscordCategoriesAndChannels`. The reason this API has been added is because discord servers can have a lot of message and channel data, which causes `ExtractDiscordCategoriesAndChannels` to block the thread for too long, making apps potentially feel like they are stuck. This API runs inside a go routine, eventually calls `ExtractDiscordCategoriesAndChannels`, and then emits a newly introduced `DiscordCategoriesAndChannelsExtractedSignal` that clients can react to. Failure of extraction has to be determined by the `discord.ImportErrors` emitted by the signal. **A note about exported discord history files** We expect users to export their discord histories via the [DiscordChatExporter](https://github.com/Tyrrrz/DiscordChatExporter/wiki/GUI%2C-CLI-and-Formats-explained#exportguild) tool. The tool allows to export the data in different formats, such as JSON, HTML and CSV. We expect users to have their data exported as JSON. Closes: https://github.com/status-im/status-desktop/issues/6690
2022-07-13 09:33:53 +00:00
type DiscordMessageAuthor struct {
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Discriminator string `protobuf:"bytes,3,opt,name=discriminator,proto3" json:"discriminator,omitempty"`
Nickname string `protobuf:"bytes,4,opt,name=nickname,proto3" json:"nickname,omitempty"`
AvatarUrl string `protobuf:"bytes,5,opt,name=avatarUrl,proto3" json:"avatarUrl,omitempty"`
AvatarImagePayload []byte `protobuf:"bytes,6,opt,name=avatarImagePayload,proto3" json:"avatarImagePayload,omitempty"`
LocalUrl string `protobuf:"bytes,7,opt,name=localUrl,proto3" json:"localUrl,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *DiscordMessageAuthor) Reset() { *m = DiscordMessageAuthor{} }
func (m *DiscordMessageAuthor) String() string { return proto.CompactTextString(m) }
func (*DiscordMessageAuthor) ProtoMessage() {}
func (*DiscordMessageAuthor) Descriptor() ([]byte, []int) {
return fileDescriptor_263952f55fd35689, []int{7}
}
func (m *DiscordMessageAuthor) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_DiscordMessageAuthor.Unmarshal(m, b)
}
func (m *DiscordMessageAuthor) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_DiscordMessageAuthor.Marshal(b, m, deterministic)
}
func (m *DiscordMessageAuthor) XXX_Merge(src proto.Message) {
xxx_messageInfo_DiscordMessageAuthor.Merge(m, src)
}
func (m *DiscordMessageAuthor) XXX_Size() int {
return xxx_messageInfo_DiscordMessageAuthor.Size(m)
}
func (m *DiscordMessageAuthor) XXX_DiscardUnknown() {
xxx_messageInfo_DiscordMessageAuthor.DiscardUnknown(m)
}
var xxx_messageInfo_DiscordMessageAuthor proto.InternalMessageInfo
func (m *DiscordMessageAuthor) GetId() string {
if m != nil {
return m.Id
feat: introduce messenger APIs to extract discord channels As part of the new Discord <-> Status Community Import functionality, we're adding an API that extracts all discord categories and channels from a previously exported discord export file. These APIs can be used in clients to show the user what categories and channels will be imported later on. There are two APIs: 1. `Messenger.ExtractDiscordCategoriesAndChannels(filesToimport []string) (*MessengerResponse, map[string]*discord.ImportError)` This takes a list of exported discord export (JSON) files (typically one per channel), reads them, and extracts the categories and channels into dedicated data structures (`[]DiscordChannel` and `[]DiscordCategory`) It also returns the oldest message timestamp found in all extracted channels. The API is synchronous and returns the extracted data as a `*MessengerResponse`. This allows to make the API available status-go's RPC interface. The error case is a `map[string]*discord.ImportError` where each key is a file path of a JSON file that we tried to extract data from, and the value a `discord.ImportError` which holds an error message and an error code, allowing for distinguishing between "critical" errors and "non-critical" errors. 2. `Messenger.RequestExtractDiscordCategoriesAndChannels(filesToImport []string)` This is the asynchronous counterpart to `ExtractDiscordCategoriesAndChannels`. The reason this API has been added is because discord servers can have a lot of message and channel data, which causes `ExtractDiscordCategoriesAndChannels` to block the thread for too long, making apps potentially feel like they are stuck. This API runs inside a go routine, eventually calls `ExtractDiscordCategoriesAndChannels`, and then emits a newly introduced `DiscordCategoriesAndChannelsExtractedSignal` that clients can react to. Failure of extraction has to be determined by the `discord.ImportErrors` emitted by the signal. **A note about exported discord history files** We expect users to export their discord histories via the [DiscordChatExporter](https://github.com/Tyrrrz/DiscordChatExporter/wiki/GUI%2C-CLI-and-Formats-explained#exportguild) tool. The tool allows to export the data in different formats, such as JSON, HTML and CSV. We expect users to have their data exported as JSON. Closes: https://github.com/status-im/status-desktop/issues/6690
2022-07-13 09:33:53 +00:00
}
return ""
}
func (m *DiscordMessageAuthor) GetName() string {
if m != nil {
return m.Name
feat: introduce messenger APIs to extract discord channels As part of the new Discord <-> Status Community Import functionality, we're adding an API that extracts all discord categories and channels from a previously exported discord export file. These APIs can be used in clients to show the user what categories and channels will be imported later on. There are two APIs: 1. `Messenger.ExtractDiscordCategoriesAndChannels(filesToimport []string) (*MessengerResponse, map[string]*discord.ImportError)` This takes a list of exported discord export (JSON) files (typically one per channel), reads them, and extracts the categories and channels into dedicated data structures (`[]DiscordChannel` and `[]DiscordCategory`) It also returns the oldest message timestamp found in all extracted channels. The API is synchronous and returns the extracted data as a `*MessengerResponse`. This allows to make the API available status-go's RPC interface. The error case is a `map[string]*discord.ImportError` where each key is a file path of a JSON file that we tried to extract data from, and the value a `discord.ImportError` which holds an error message and an error code, allowing for distinguishing between "critical" errors and "non-critical" errors. 2. `Messenger.RequestExtractDiscordCategoriesAndChannels(filesToImport []string)` This is the asynchronous counterpart to `ExtractDiscordCategoriesAndChannels`. The reason this API has been added is because discord servers can have a lot of message and channel data, which causes `ExtractDiscordCategoriesAndChannels` to block the thread for too long, making apps potentially feel like they are stuck. This API runs inside a go routine, eventually calls `ExtractDiscordCategoriesAndChannels`, and then emits a newly introduced `DiscordCategoriesAndChannelsExtractedSignal` that clients can react to. Failure of extraction has to be determined by the `discord.ImportErrors` emitted by the signal. **A note about exported discord history files** We expect users to export their discord histories via the [DiscordChatExporter](https://github.com/Tyrrrz/DiscordChatExporter/wiki/GUI%2C-CLI-and-Formats-explained#exportguild) tool. The tool allows to export the data in different formats, such as JSON, HTML and CSV. We expect users to have their data exported as JSON. Closes: https://github.com/status-im/status-desktop/issues/6690
2022-07-13 09:33:53 +00:00
}
return ""
}
func (m *DiscordMessageAuthor) GetDiscriminator() string {
if m != nil {
return m.Discriminator
feat: introduce messenger APIs to extract discord channels As part of the new Discord <-> Status Community Import functionality, we're adding an API that extracts all discord categories and channels from a previously exported discord export file. These APIs can be used in clients to show the user what categories and channels will be imported later on. There are two APIs: 1. `Messenger.ExtractDiscordCategoriesAndChannels(filesToimport []string) (*MessengerResponse, map[string]*discord.ImportError)` This takes a list of exported discord export (JSON) files (typically one per channel), reads them, and extracts the categories and channels into dedicated data structures (`[]DiscordChannel` and `[]DiscordCategory`) It also returns the oldest message timestamp found in all extracted channels. The API is synchronous and returns the extracted data as a `*MessengerResponse`. This allows to make the API available status-go's RPC interface. The error case is a `map[string]*discord.ImportError` where each key is a file path of a JSON file that we tried to extract data from, and the value a `discord.ImportError` which holds an error message and an error code, allowing for distinguishing between "critical" errors and "non-critical" errors. 2. `Messenger.RequestExtractDiscordCategoriesAndChannels(filesToImport []string)` This is the asynchronous counterpart to `ExtractDiscordCategoriesAndChannels`. The reason this API has been added is because discord servers can have a lot of message and channel data, which causes `ExtractDiscordCategoriesAndChannels` to block the thread for too long, making apps potentially feel like they are stuck. This API runs inside a go routine, eventually calls `ExtractDiscordCategoriesAndChannels`, and then emits a newly introduced `DiscordCategoriesAndChannelsExtractedSignal` that clients can react to. Failure of extraction has to be determined by the `discord.ImportErrors` emitted by the signal. **A note about exported discord history files** We expect users to export their discord histories via the [DiscordChatExporter](https://github.com/Tyrrrz/DiscordChatExporter/wiki/GUI%2C-CLI-and-Formats-explained#exportguild) tool. The tool allows to export the data in different formats, such as JSON, HTML and CSV. We expect users to have their data exported as JSON. Closes: https://github.com/status-im/status-desktop/issues/6690
2022-07-13 09:33:53 +00:00
}
return ""
}
func (m *DiscordMessageAuthor) GetNickname() string {
if m != nil {
return m.Nickname
feat: introduce messenger APIs to extract discord channels As part of the new Discord <-> Status Community Import functionality, we're adding an API that extracts all discord categories and channels from a previously exported discord export file. These APIs can be used in clients to show the user what categories and channels will be imported later on. There are two APIs: 1. `Messenger.ExtractDiscordCategoriesAndChannels(filesToimport []string) (*MessengerResponse, map[string]*discord.ImportError)` This takes a list of exported discord export (JSON) files (typically one per channel), reads them, and extracts the categories and channels into dedicated data structures (`[]DiscordChannel` and `[]DiscordCategory`) It also returns the oldest message timestamp found in all extracted channels. The API is synchronous and returns the extracted data as a `*MessengerResponse`. This allows to make the API available status-go's RPC interface. The error case is a `map[string]*discord.ImportError` where each key is a file path of a JSON file that we tried to extract data from, and the value a `discord.ImportError` which holds an error message and an error code, allowing for distinguishing between "critical" errors and "non-critical" errors. 2. `Messenger.RequestExtractDiscordCategoriesAndChannels(filesToImport []string)` This is the asynchronous counterpart to `ExtractDiscordCategoriesAndChannels`. The reason this API has been added is because discord servers can have a lot of message and channel data, which causes `ExtractDiscordCategoriesAndChannels` to block the thread for too long, making apps potentially feel like they are stuck. This API runs inside a go routine, eventually calls `ExtractDiscordCategoriesAndChannels`, and then emits a newly introduced `DiscordCategoriesAndChannelsExtractedSignal` that clients can react to. Failure of extraction has to be determined by the `discord.ImportErrors` emitted by the signal. **A note about exported discord history files** We expect users to export their discord histories via the [DiscordChatExporter](https://github.com/Tyrrrz/DiscordChatExporter/wiki/GUI%2C-CLI-and-Formats-explained#exportguild) tool. The tool allows to export the data in different formats, such as JSON, HTML and CSV. We expect users to have their data exported as JSON. Closes: https://github.com/status-im/status-desktop/issues/6690
2022-07-13 09:33:53 +00:00
}
return ""
}
func (m *DiscordMessageAuthor) GetAvatarUrl() string {
if m != nil {
return m.AvatarUrl
feat: introduce messenger APIs to extract discord channels As part of the new Discord <-> Status Community Import functionality, we're adding an API that extracts all discord categories and channels from a previously exported discord export file. These APIs can be used in clients to show the user what categories and channels will be imported later on. There are two APIs: 1. `Messenger.ExtractDiscordCategoriesAndChannels(filesToimport []string) (*MessengerResponse, map[string]*discord.ImportError)` This takes a list of exported discord export (JSON) files (typically one per channel), reads them, and extracts the categories and channels into dedicated data structures (`[]DiscordChannel` and `[]DiscordCategory`) It also returns the oldest message timestamp found in all extracted channels. The API is synchronous and returns the extracted data as a `*MessengerResponse`. This allows to make the API available status-go's RPC interface. The error case is a `map[string]*discord.ImportError` where each key is a file path of a JSON file that we tried to extract data from, and the value a `discord.ImportError` which holds an error message and an error code, allowing for distinguishing between "critical" errors and "non-critical" errors. 2. `Messenger.RequestExtractDiscordCategoriesAndChannels(filesToImport []string)` This is the asynchronous counterpart to `ExtractDiscordCategoriesAndChannels`. The reason this API has been added is because discord servers can have a lot of message and channel data, which causes `ExtractDiscordCategoriesAndChannels` to block the thread for too long, making apps potentially feel like they are stuck. This API runs inside a go routine, eventually calls `ExtractDiscordCategoriesAndChannels`, and then emits a newly introduced `DiscordCategoriesAndChannelsExtractedSignal` that clients can react to. Failure of extraction has to be determined by the `discord.ImportErrors` emitted by the signal. **A note about exported discord history files** We expect users to export their discord histories via the [DiscordChatExporter](https://github.com/Tyrrrz/DiscordChatExporter/wiki/GUI%2C-CLI-and-Formats-explained#exportguild) tool. The tool allows to export the data in different formats, such as JSON, HTML and CSV. We expect users to have their data exported as JSON. Closes: https://github.com/status-im/status-desktop/issues/6690
2022-07-13 09:33:53 +00:00
}
return ""
}
func (m *DiscordMessageAuthor) GetAvatarImagePayload() []byte {
if m != nil {
return m.AvatarImagePayload
}
return nil
}
func (m *DiscordMessageAuthor) GetLocalUrl() string {
if m != nil {
return m.LocalUrl
}
return ""
}
type DiscordMessageReference struct {
MessageId string `protobuf:"bytes,1,opt,name=messageId,proto3" json:"messageId,omitempty"`
ChannelId string `protobuf:"bytes,2,opt,name=channelId,proto3" json:"channelId,omitempty"`
GuildId string `protobuf:"bytes,3,opt,name=guildId,proto3" json:"guildId,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *DiscordMessageReference) Reset() { *m = DiscordMessageReference{} }
func (m *DiscordMessageReference) String() string { return proto.CompactTextString(m) }
func (*DiscordMessageReference) ProtoMessage() {}
func (*DiscordMessageReference) Descriptor() ([]byte, []int) {
return fileDescriptor_263952f55fd35689, []int{8}
}
func (m *DiscordMessageReference) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_DiscordMessageReference.Unmarshal(m, b)
}
func (m *DiscordMessageReference) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_DiscordMessageReference.Marshal(b, m, deterministic)
}
func (m *DiscordMessageReference) XXX_Merge(src proto.Message) {
xxx_messageInfo_DiscordMessageReference.Merge(m, src)
}
func (m *DiscordMessageReference) XXX_Size() int {
return xxx_messageInfo_DiscordMessageReference.Size(m)
}
func (m *DiscordMessageReference) XXX_DiscardUnknown() {
xxx_messageInfo_DiscordMessageReference.DiscardUnknown(m)
}
var xxx_messageInfo_DiscordMessageReference proto.InternalMessageInfo
func (m *DiscordMessageReference) GetMessageId() string {
if m != nil {
return m.MessageId
}
return ""
}
func (m *DiscordMessageReference) GetChannelId() string {
if m != nil {
return m.ChannelId
}
return ""
}
func (m *DiscordMessageReference) GetGuildId() string {
if m != nil {
return m.GuildId
}
return ""
}
type DiscordMessageAttachment struct {
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
MessageId string `protobuf:"bytes,2,opt,name=messageId,proto3" json:"messageId,omitempty"`
Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"`
FileName string `protobuf:"bytes,4,opt,name=fileName,proto3" json:"fileName,omitempty"`
FileSizeBytes uint64 `protobuf:"varint,5,opt,name=fileSizeBytes,proto3" json:"fileSizeBytes,omitempty"`
ContentType string `protobuf:"bytes,6,opt,name=contentType,proto3" json:"contentType,omitempty"`
Payload []byte `protobuf:"bytes,7,opt,name=payload,proto3" json:"payload,omitempty"`
LocalUrl string `protobuf:"bytes,8,opt,name=localUrl,proto3" json:"localUrl,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *DiscordMessageAttachment) Reset() { *m = DiscordMessageAttachment{} }
func (m *DiscordMessageAttachment) String() string { return proto.CompactTextString(m) }
func (*DiscordMessageAttachment) ProtoMessage() {}
func (*DiscordMessageAttachment) Descriptor() ([]byte, []int) {
return fileDescriptor_263952f55fd35689, []int{9}
}
func (m *DiscordMessageAttachment) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_DiscordMessageAttachment.Unmarshal(m, b)
}
func (m *DiscordMessageAttachment) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_DiscordMessageAttachment.Marshal(b, m, deterministic)
}
func (m *DiscordMessageAttachment) XXX_Merge(src proto.Message) {
xxx_messageInfo_DiscordMessageAttachment.Merge(m, src)
}
func (m *DiscordMessageAttachment) XXX_Size() int {
return xxx_messageInfo_DiscordMessageAttachment.Size(m)
}
func (m *DiscordMessageAttachment) XXX_DiscardUnknown() {
xxx_messageInfo_DiscordMessageAttachment.DiscardUnknown(m)
}
var xxx_messageInfo_DiscordMessageAttachment proto.InternalMessageInfo
func (m *DiscordMessageAttachment) GetId() string {
if m != nil {
return m.Id
}
return ""
}
func (m *DiscordMessageAttachment) GetMessageId() string {
if m != nil {
return m.MessageId
}
return ""
}
func (m *DiscordMessageAttachment) GetUrl() string {
if m != nil {
return m.Url
}
return ""
}
func (m *DiscordMessageAttachment) GetFileName() string {
if m != nil {
return m.FileName
}
return ""
}
func (m *DiscordMessageAttachment) GetFileSizeBytes() uint64 {
if m != nil {
return m.FileSizeBytes
}
return 0
}
func (m *DiscordMessageAttachment) GetContentType() string {
if m != nil {
return m.ContentType
}
return ""
}
func (m *DiscordMessageAttachment) GetPayload() []byte {
if m != nil {
return m.Payload
}
return nil
}
func (m *DiscordMessageAttachment) GetLocalUrl() string {
if m != nil {
return m.LocalUrl
}
return ""
}
2023-10-13 12:25:34 +00:00
type UnfurledLinkThumbnail struct {
Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"`
Width uint32 `protobuf:"varint,2,opt,name=width,proto3" json:"width,omitempty"`
Height uint32 `protobuf:"varint,3,opt,name=height,proto3" json:"height,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *UnfurledLinkThumbnail) Reset() { *m = UnfurledLinkThumbnail{} }
func (m *UnfurledLinkThumbnail) String() string { return proto.CompactTextString(m) }
func (*UnfurledLinkThumbnail) ProtoMessage() {}
func (*UnfurledLinkThumbnail) Descriptor() ([]byte, []int) {
return fileDescriptor_263952f55fd35689, []int{10}
}
func (m *UnfurledLinkThumbnail) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_UnfurledLinkThumbnail.Unmarshal(m, b)
}
func (m *UnfurledLinkThumbnail) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_UnfurledLinkThumbnail.Marshal(b, m, deterministic)
}
func (m *UnfurledLinkThumbnail) XXX_Merge(src proto.Message) {
xxx_messageInfo_UnfurledLinkThumbnail.Merge(m, src)
}
func (m *UnfurledLinkThumbnail) XXX_Size() int {
return xxx_messageInfo_UnfurledLinkThumbnail.Size(m)
}
func (m *UnfurledLinkThumbnail) XXX_DiscardUnknown() {
xxx_messageInfo_UnfurledLinkThumbnail.DiscardUnknown(m)
}
var xxx_messageInfo_UnfurledLinkThumbnail proto.InternalMessageInfo
2023-10-13 12:25:34 +00:00
func (m *UnfurledLinkThumbnail) GetPayload() []byte {
if m != nil {
return m.Payload
2023-10-13 12:25:34 +00:00
}
return nil
}
func (m *UnfurledLinkThumbnail) GetWidth() uint32 {
if m != nil {
return m.Width
2023-10-13 12:25:34 +00:00
}
return 0
}
func (m *UnfurledLinkThumbnail) GetHeight() uint32 {
if m != nil {
return m.Height
2023-10-13 12:25:34 +00:00
}
return 0
}
URL unfurling (initial implementation) (#3471) This is the initial implementation for the new URL unfurling requirements. The most important one is that only the message sender will pay the privacy cost for unfurling and extracting metadata from websites. Once the message is sent, the unfurled data will be stored at the protocol level and receivers will just profit and happily decode the metadata to render it. Further development of this URL unfurling capability will be mostly guided by issues created on clients. For the moment in status-mobile: https://github.com/status-im/status-mobile/labels/url-preview - https://github.com/status-im/status-mobile/issues/15918 - https://github.com/status-im/status-mobile/issues/15917 - https://github.com/status-im/status-mobile/issues/15910 - https://github.com/status-im/status-mobile/issues/15909 - https://github.com/status-im/status-mobile/issues/15908 - https://github.com/status-im/status-mobile/issues/15906 - https://github.com/status-im/status-mobile/issues/15905 ### Terminology In the code, I've tried to stick to the word "unfurl URL" to really mean the process of extracting metadata from a website, sort of lower level. I use "link preview" to mean a higher level structure which is enriched by unfurled data. "link preview" is also how designers refer to it. ### User flows 1. Carol needs to see link previews while typing in the chat input field. Notice from the diagram nothing is persisted and that status-go endpoints are essentially stateless. ``` #+begin_src plantuml :results verbatim Client->>Server: Call wakuext_getTextURLs Server-->>Client: Normalized URLs Client->>Client: Render cached unfurled URLs Client->>Server: Unfurl non-cached URLs.\nCall wakuext_unfurlURLs Server->>Website: Fetch metadata Website-->>Server: Metadata (thumbnail URL, title, etc) Server->>Website: Fetch thumbnail Server->>Website: Fetch favicon Website-->>Server: Favicon bytes Website-->>Server: Thumbnail bytes Server->>Server: Decode & process images Server-->>Client: Unfurled data (thumbnail data URI, etc) #+end_src ``` ``` ,------. ,------. ,-------. |Client| |Server| |Website| `--+---' `--+---' `---+---' | Call wakuext_getTextURLs | | | ---------------------------------------> | | | | | Normalized URLs | | | <- - - - - - - - - - - - - - - - - - - - | | | | |----. | | | | Render cached unfurled URLs | | |<---' | | | | | | Unfurl non-cached URLs. | | | Call wakuext_unfurlURLs | | | ---------------------------------------> | | | | | | Fetch metadata | | | ------------------------------------> | | | | | Metadata (thumbnail URL, title, etc)| | | <- - - - - - - - - - - - - - - - - - | | | | | Fetch thumbnail | | | ------------------------------------> | | | | | Fetch favicon | | | ------------------------------------> | | | | | Favicon bytes | | | <- - - - - - - - - - - - - - - - - - | | | | | Thumbnail bytes | | | <- - - - - - - - - - - - - - - - - - | | | | |----. | | | | Decode & process images | | |<---' | | | | | Unfurled data (thumbnail data URI, etc)| | | <- - - - - - - - - - - - - - - - - - - - | ,--+---. ,--+---. ,---+---. |Client| |Server| |Website| `------' `------' `-------' ``` 2. Carol sends the text message with link previews in the RPC request wakuext_sendChatMessages. status-go assumes the link previews are good because it can't and shouldn't attempt to re-unfurl them. ``` #+begin_src plantuml :results verbatim Client->>Server: Call wakuext_sendChatMessages Server->>Server: Transform link previews to\nbe proto-marshalled Server->DB: Write link previews serialized as JSON Server-->>Client: Updated message response #+end_src ``` ``` ,------. ,------. ,--. |Client| |Server| |DB| `--+---' `--+---' `+-' | Call wakuext_sendChatMessages| | | -----------------------------> | | | | | |----. | | | | Transform link previews to | | |<---' be proto-marshalled | | | | | | | | | Write link previews serialized as JSON| | | --------------------------------------> | | | | Updated message response | | | <- - - - - - - - - - - - - - - | ,--+---. ,--+---. ,+-. |Client| |Server| |DB| `------' `------' `--' ``` 3. The message was sent over waku and persisted locally in Carol's device. She should now see the link previews in the chat history. There can be many link previews shared by other chat members, therefore it is important to serve the assets via the media server to avoid overloading the ReactNative bridge with lots of big JSON payloads containing base64 encoded data URIs (maybe this concern is meaningless for desktop). When a client is rendering messages with link previews, they will have the field linkPreviews, and the thumbnail URL will point to the local media server. ``` #+begin_src plantuml :results verbatim Client->>Server: GET /link-preview/thumbnail (media server) Server->>DB: Read from user_messages.unfurled_links Server->Server: Unmarshal JSON Server-->>Client: HTTP Content-Type: image/jpeg/etc #+end_src ``` ``` ,------. ,------. ,--. |Client| |Server| |DB| `--+---' `--+---' `+-' | GET /link-preview/thumbnail (media server)| | | ------------------------------------------> | | | | | | Read from user_messages.unfurled_links| | | --------------------------------------> | | | | |----. | | | | Unmarshal JSON | | |<---' | | | | | HTTP Content-Type: image/jpeg/etc | | | <- - - - - - - - - - - - - - - - - - - - - | ,--+---. ,--+---. ,+-. |Client| |Server| |DB| `------' `------' `--' ``` ### Some limitations of the current implementation The following points will become separate issues in status-go that I'll work on over the next couple weeks. In no order of importance: - Improve how multiple links are fetched; retries on failure and testing how unfurling behaves around the timeout limits (deterministically, not by making real HTTP calls as I did). https://github.com/status-im/status-go/issues/3498 - Unfurl favicons and store them in the protobuf too. - For this PR, I added unfurling support only for websites with OpenGraph https://ogp.me/ meta tags. Other unfurlers will be implemented on demand. The next one will probably be for oEmbed https://oembed.com/, the protocol supported by YouTube, for example. - Resize and/or compress thumbnails (and favicons). Often times, thumbnails are huge for the purposes of link previews. There is already support for compressing JPEGs in status-go, but I prefer to work with compression in a separate PR because I'd like to also solve the problem for PNGs (probably convert them to JPEGs, plus compress them). This would be a safe choice for thumbnails, favicons not so much because transparency is desirable. - Editing messages is not yet supported. - I haven't coded any artificial limit on the number of previews or on the size of the thumbnail payload. This will be done in a separate issue. I have heard the ideal solution may be to split messages into smaller chunks of ~125 KiB because of libp2p, but that might be too complicated at this stage of the product (?). - Link preview deletion. - For the moment, OpenGraph metadata is extracted by requesting data for the English language (and fallback to whatever is available). In the future, we'll want to unfurl by respecting the user's local device language. Some websites, like GoDaddy, are already localized based on the device's IP, but many aren't. - The website's description text should be limited by a certain number of characters, especially because it's outside our control. Exactly how much has not been decided yet, so it'll be done separately. - URL normalization can be tricky, so I implemented only the basics to help with caching. For example, the url https://status.im and HTTPS://status.im are considered identical. Also, a URL is considered valid for unfurling if its TLD exists according to publicsuffix.EffectiveTLDPlusOne. This was essential, otherwise the default Go url.Parse approach would consider many invalid URLs valid, and thus the server would waste resources trying to unfurl the unfurleable. ### Other requirements - If the message is edited, the link previews should reflect the edited text, not the original one. This has been aligned with the design team as well. - If the website's thumbnail or the favicon can't be fetched, just ignore them. The only mandatory piece of metadata is the website's title and URL. - Link previews in clients should be generated in near real-time, that is, as the user types, previews are updated. In mobile this performs very well, and it's what other clients like WhatsApp, Telegram, and Facebook do. ### Decisions - While the user typing in the input field, the client is constantly (debounced) asking status-go to parse the text and extract normalized URLs and then the client checks if they're already in its in-memory cache. If they are, no RPC call is made. I chose this approach to achieve the best possible performance in mobile and avoid the whole RPC overhead, since the chat experience is already not smooth enough. The mobile client uses URLs as cache keys in a hashmap, i.e. if the key is present, it means the preview is readily available (naive, but good enough for now). This decision also gave me more flexibility to find the best UX at this stage of the feature. - Due to the requirement that users should be able to see independent loading indicators for each link preview, when status-go can't unfurl a URL, it doesn't return it in the response. - As an initial implementation, I added the BLOB column unfurled_links to the user_messages table. The preview data is then serialized as JSON before being stored in this column. I felt that creating a separate table and the related code for this initial PR would be inconvenient. Is that reasonable to you? Once things stabilize I can create a proper table if we want to avoid this kind of solution with serialized columns.
2023-05-18 18:43:06 +00:00
type UnfurledLink struct {
// A valid URL which uniquely identifies this link.
Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
// Website's title.
Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
// Description is sometimes available, but can be empty. Most mainstream
// websites provide this information.
Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"`
ThumbnailPayload []byte `protobuf:"bytes,4,opt,name=thumbnail_payload,json=thumbnailPayload,proto3" json:"thumbnail_payload,omitempty"`
ThumbnailWidth uint32 `protobuf:"varint,5,opt,name=thumbnail_width,json=thumbnailWidth,proto3" json:"thumbnail_width,omitempty"`
ThumbnailHeight uint32 `protobuf:"varint,6,opt,name=thumbnail_height,json=thumbnailHeight,proto3" json:"thumbnail_height,omitempty"`
Type UnfurledLink_LinkType `protobuf:"varint,7,opt,name=type,proto3,enum=protobuf.UnfurledLink_LinkType" json:"type,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *UnfurledLink) Reset() { *m = UnfurledLink{} }
func (m *UnfurledLink) String() string { return proto.CompactTextString(m) }
func (*UnfurledLink) ProtoMessage() {}
func (*UnfurledLink) Descriptor() ([]byte, []int) {
return fileDescriptor_263952f55fd35689, []int{11}
URL unfurling (initial implementation) (#3471) This is the initial implementation for the new URL unfurling requirements. The most important one is that only the message sender will pay the privacy cost for unfurling and extracting metadata from websites. Once the message is sent, the unfurled data will be stored at the protocol level and receivers will just profit and happily decode the metadata to render it. Further development of this URL unfurling capability will be mostly guided by issues created on clients. For the moment in status-mobile: https://github.com/status-im/status-mobile/labels/url-preview - https://github.com/status-im/status-mobile/issues/15918 - https://github.com/status-im/status-mobile/issues/15917 - https://github.com/status-im/status-mobile/issues/15910 - https://github.com/status-im/status-mobile/issues/15909 - https://github.com/status-im/status-mobile/issues/15908 - https://github.com/status-im/status-mobile/issues/15906 - https://github.com/status-im/status-mobile/issues/15905 ### Terminology In the code, I've tried to stick to the word "unfurl URL" to really mean the process of extracting metadata from a website, sort of lower level. I use "link preview" to mean a higher level structure which is enriched by unfurled data. "link preview" is also how designers refer to it. ### User flows 1. Carol needs to see link previews while typing in the chat input field. Notice from the diagram nothing is persisted and that status-go endpoints are essentially stateless. ``` #+begin_src plantuml :results verbatim Client->>Server: Call wakuext_getTextURLs Server-->>Client: Normalized URLs Client->>Client: Render cached unfurled URLs Client->>Server: Unfurl non-cached URLs.\nCall wakuext_unfurlURLs Server->>Website: Fetch metadata Website-->>Server: Metadata (thumbnail URL, title, etc) Server->>Website: Fetch thumbnail Server->>Website: Fetch favicon Website-->>Server: Favicon bytes Website-->>Server: Thumbnail bytes Server->>Server: Decode & process images Server-->>Client: Unfurled data (thumbnail data URI, etc) #+end_src ``` ``` ,------. ,------. ,-------. |Client| |Server| |Website| `--+---' `--+---' `---+---' | Call wakuext_getTextURLs | | | ---------------------------------------> | | | | | Normalized URLs | | | <- - - - - - - - - - - - - - - - - - - - | | | | |----. | | | | Render cached unfurled URLs | | |<---' | | | | | | Unfurl non-cached URLs. | | | Call wakuext_unfurlURLs | | | ---------------------------------------> | | | | | | Fetch metadata | | | ------------------------------------> | | | | | Metadata (thumbnail URL, title, etc)| | | <- - - - - - - - - - - - - - - - - - | | | | | Fetch thumbnail | | | ------------------------------------> | | | | | Fetch favicon | | | ------------------------------------> | | | | | Favicon bytes | | | <- - - - - - - - - - - - - - - - - - | | | | | Thumbnail bytes | | | <- - - - - - - - - - - - - - - - - - | | | | |----. | | | | Decode & process images | | |<---' | | | | | Unfurled data (thumbnail data URI, etc)| | | <- - - - - - - - - - - - - - - - - - - - | ,--+---. ,--+---. ,---+---. |Client| |Server| |Website| `------' `------' `-------' ``` 2. Carol sends the text message with link previews in the RPC request wakuext_sendChatMessages. status-go assumes the link previews are good because it can't and shouldn't attempt to re-unfurl them. ``` #+begin_src plantuml :results verbatim Client->>Server: Call wakuext_sendChatMessages Server->>Server: Transform link previews to\nbe proto-marshalled Server->DB: Write link previews serialized as JSON Server-->>Client: Updated message response #+end_src ``` ``` ,------. ,------. ,--. |Client| |Server| |DB| `--+---' `--+---' `+-' | Call wakuext_sendChatMessages| | | -----------------------------> | | | | | |----. | | | | Transform link previews to | | |<---' be proto-marshalled | | | | | | | | | Write link previews serialized as JSON| | | --------------------------------------> | | | | Updated message response | | | <- - - - - - - - - - - - - - - | ,--+---. ,--+---. ,+-. |Client| |Server| |DB| `------' `------' `--' ``` 3. The message was sent over waku and persisted locally in Carol's device. She should now see the link previews in the chat history. There can be many link previews shared by other chat members, therefore it is important to serve the assets via the media server to avoid overloading the ReactNative bridge with lots of big JSON payloads containing base64 encoded data URIs (maybe this concern is meaningless for desktop). When a client is rendering messages with link previews, they will have the field linkPreviews, and the thumbnail URL will point to the local media server. ``` #+begin_src plantuml :results verbatim Client->>Server: GET /link-preview/thumbnail (media server) Server->>DB: Read from user_messages.unfurled_links Server->Server: Unmarshal JSON Server-->>Client: HTTP Content-Type: image/jpeg/etc #+end_src ``` ``` ,------. ,------. ,--. |Client| |Server| |DB| `--+---' `--+---' `+-' | GET /link-preview/thumbnail (media server)| | | ------------------------------------------> | | | | | | Read from user_messages.unfurled_links| | | --------------------------------------> | | | | |----. | | | | Unmarshal JSON | | |<---' | | | | | HTTP Content-Type: image/jpeg/etc | | | <- - - - - - - - - - - - - - - - - - - - - | ,--+---. ,--+---. ,+-. |Client| |Server| |DB| `------' `------' `--' ``` ### Some limitations of the current implementation The following points will become separate issues in status-go that I'll work on over the next couple weeks. In no order of importance: - Improve how multiple links are fetched; retries on failure and testing how unfurling behaves around the timeout limits (deterministically, not by making real HTTP calls as I did). https://github.com/status-im/status-go/issues/3498 - Unfurl favicons and store them in the protobuf too. - For this PR, I added unfurling support only for websites with OpenGraph https://ogp.me/ meta tags. Other unfurlers will be implemented on demand. The next one will probably be for oEmbed https://oembed.com/, the protocol supported by YouTube, for example. - Resize and/or compress thumbnails (and favicons). Often times, thumbnails are huge for the purposes of link previews. There is already support for compressing JPEGs in status-go, but I prefer to work with compression in a separate PR because I'd like to also solve the problem for PNGs (probably convert them to JPEGs, plus compress them). This would be a safe choice for thumbnails, favicons not so much because transparency is desirable. - Editing messages is not yet supported. - I haven't coded any artificial limit on the number of previews or on the size of the thumbnail payload. This will be done in a separate issue. I have heard the ideal solution may be to split messages into smaller chunks of ~125 KiB because of libp2p, but that might be too complicated at this stage of the product (?). - Link preview deletion. - For the moment, OpenGraph metadata is extracted by requesting data for the English language (and fallback to whatever is available). In the future, we'll want to unfurl by respecting the user's local device language. Some websites, like GoDaddy, are already localized based on the device's IP, but many aren't. - The website's description text should be limited by a certain number of characters, especially because it's outside our control. Exactly how much has not been decided yet, so it'll be done separately. - URL normalization can be tricky, so I implemented only the basics to help with caching. For example, the url https://status.im and HTTPS://status.im are considered identical. Also, a URL is considered valid for unfurling if its TLD exists according to publicsuffix.EffectiveTLDPlusOne. This was essential, otherwise the default Go url.Parse approach would consider many invalid URLs valid, and thus the server would waste resources trying to unfurl the unfurleable. ### Other requirements - If the message is edited, the link previews should reflect the edited text, not the original one. This has been aligned with the design team as well. - If the website's thumbnail or the favicon can't be fetched, just ignore them. The only mandatory piece of metadata is the website's title and URL. - Link previews in clients should be generated in near real-time, that is, as the user types, previews are updated. In mobile this performs very well, and it's what other clients like WhatsApp, Telegram, and Facebook do. ### Decisions - While the user typing in the input field, the client is constantly (debounced) asking status-go to parse the text and extract normalized URLs and then the client checks if they're already in its in-memory cache. If they are, no RPC call is made. I chose this approach to achieve the best possible performance in mobile and avoid the whole RPC overhead, since the chat experience is already not smooth enough. The mobile client uses URLs as cache keys in a hashmap, i.e. if the key is present, it means the preview is readily available (naive, but good enough for now). This decision also gave me more flexibility to find the best UX at this stage of the feature. - Due to the requirement that users should be able to see independent loading indicators for each link preview, when status-go can't unfurl a URL, it doesn't return it in the response. - As an initial implementation, I added the BLOB column unfurled_links to the user_messages table. The preview data is then serialized as JSON before being stored in this column. I felt that creating a separate table and the related code for this initial PR would be inconvenient. Is that reasonable to you? Once things stabilize I can create a proper table if we want to avoid this kind of solution with serialized columns.
2023-05-18 18:43:06 +00:00
}
func (m *UnfurledLink) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_UnfurledLink.Unmarshal(m, b)
}
func (m *UnfurledLink) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_UnfurledLink.Marshal(b, m, deterministic)
}
func (m *UnfurledLink) XXX_Merge(src proto.Message) {
xxx_messageInfo_UnfurledLink.Merge(m, src)
}
func (m *UnfurledLink) XXX_Size() int {
return xxx_messageInfo_UnfurledLink.Size(m)
}
func (m *UnfurledLink) XXX_DiscardUnknown() {
xxx_messageInfo_UnfurledLink.DiscardUnknown(m)
}
var xxx_messageInfo_UnfurledLink proto.InternalMessageInfo
func (m *UnfurledLink) GetUrl() string {
if m != nil {
return m.Url
URL unfurling (initial implementation) (#3471) This is the initial implementation for the new URL unfurling requirements. The most important one is that only the message sender will pay the privacy cost for unfurling and extracting metadata from websites. Once the message is sent, the unfurled data will be stored at the protocol level and receivers will just profit and happily decode the metadata to render it. Further development of this URL unfurling capability will be mostly guided by issues created on clients. For the moment in status-mobile: https://github.com/status-im/status-mobile/labels/url-preview - https://github.com/status-im/status-mobile/issues/15918 - https://github.com/status-im/status-mobile/issues/15917 - https://github.com/status-im/status-mobile/issues/15910 - https://github.com/status-im/status-mobile/issues/15909 - https://github.com/status-im/status-mobile/issues/15908 - https://github.com/status-im/status-mobile/issues/15906 - https://github.com/status-im/status-mobile/issues/15905 ### Terminology In the code, I've tried to stick to the word "unfurl URL" to really mean the process of extracting metadata from a website, sort of lower level. I use "link preview" to mean a higher level structure which is enriched by unfurled data. "link preview" is also how designers refer to it. ### User flows 1. Carol needs to see link previews while typing in the chat input field. Notice from the diagram nothing is persisted and that status-go endpoints are essentially stateless. ``` #+begin_src plantuml :results verbatim Client->>Server: Call wakuext_getTextURLs Server-->>Client: Normalized URLs Client->>Client: Render cached unfurled URLs Client->>Server: Unfurl non-cached URLs.\nCall wakuext_unfurlURLs Server->>Website: Fetch metadata Website-->>Server: Metadata (thumbnail URL, title, etc) Server->>Website: Fetch thumbnail Server->>Website: Fetch favicon Website-->>Server: Favicon bytes Website-->>Server: Thumbnail bytes Server->>Server: Decode & process images Server-->>Client: Unfurled data (thumbnail data URI, etc) #+end_src ``` ``` ,------. ,------. ,-------. |Client| |Server| |Website| `--+---' `--+---' `---+---' | Call wakuext_getTextURLs | | | ---------------------------------------> | | | | | Normalized URLs | | | <- - - - - - - - - - - - - - - - - - - - | | | | |----. | | | | Render cached unfurled URLs | | |<---' | | | | | | Unfurl non-cached URLs. | | | Call wakuext_unfurlURLs | | | ---------------------------------------> | | | | | | Fetch metadata | | | ------------------------------------> | | | | | Metadata (thumbnail URL, title, etc)| | | <- - - - - - - - - - - - - - - - - - | | | | | Fetch thumbnail | | | ------------------------------------> | | | | | Fetch favicon | | | ------------------------------------> | | | | | Favicon bytes | | | <- - - - - - - - - - - - - - - - - - | | | | | Thumbnail bytes | | | <- - - - - - - - - - - - - - - - - - | | | | |----. | | | | Decode & process images | | |<---' | | | | | Unfurled data (thumbnail data URI, etc)| | | <- - - - - - - - - - - - - - - - - - - - | ,--+---. ,--+---. ,---+---. |Client| |Server| |Website| `------' `------' `-------' ``` 2. Carol sends the text message with link previews in the RPC request wakuext_sendChatMessages. status-go assumes the link previews are good because it can't and shouldn't attempt to re-unfurl them. ``` #+begin_src plantuml :results verbatim Client->>Server: Call wakuext_sendChatMessages Server->>Server: Transform link previews to\nbe proto-marshalled Server->DB: Write link previews serialized as JSON Server-->>Client: Updated message response #+end_src ``` ``` ,------. ,------. ,--. |Client| |Server| |DB| `--+---' `--+---' `+-' | Call wakuext_sendChatMessages| | | -----------------------------> | | | | | |----. | | | | Transform link previews to | | |<---' be proto-marshalled | | | | | | | | | Write link previews serialized as JSON| | | --------------------------------------> | | | | Updated message response | | | <- - - - - - - - - - - - - - - | ,--+---. ,--+---. ,+-. |Client| |Server| |DB| `------' `------' `--' ``` 3. The message was sent over waku and persisted locally in Carol's device. She should now see the link previews in the chat history. There can be many link previews shared by other chat members, therefore it is important to serve the assets via the media server to avoid overloading the ReactNative bridge with lots of big JSON payloads containing base64 encoded data URIs (maybe this concern is meaningless for desktop). When a client is rendering messages with link previews, they will have the field linkPreviews, and the thumbnail URL will point to the local media server. ``` #+begin_src plantuml :results verbatim Client->>Server: GET /link-preview/thumbnail (media server) Server->>DB: Read from user_messages.unfurled_links Server->Server: Unmarshal JSON Server-->>Client: HTTP Content-Type: image/jpeg/etc #+end_src ``` ``` ,------. ,------. ,--. |Client| |Server| |DB| `--+---' `--+---' `+-' | GET /link-preview/thumbnail (media server)| | | ------------------------------------------> | | | | | | Read from user_messages.unfurled_links| | | --------------------------------------> | | | | |----. | | | | Unmarshal JSON | | |<---' | | | | | HTTP Content-Type: image/jpeg/etc | | | <- - - - - - - - - - - - - - - - - - - - - | ,--+---. ,--+---. ,+-. |Client| |Server| |DB| `------' `------' `--' ``` ### Some limitations of the current implementation The following points will become separate issues in status-go that I'll work on over the next couple weeks. In no order of importance: - Improve how multiple links are fetched; retries on failure and testing how unfurling behaves around the timeout limits (deterministically, not by making real HTTP calls as I did). https://github.com/status-im/status-go/issues/3498 - Unfurl favicons and store them in the protobuf too. - For this PR, I added unfurling support only for websites with OpenGraph https://ogp.me/ meta tags. Other unfurlers will be implemented on demand. The next one will probably be for oEmbed https://oembed.com/, the protocol supported by YouTube, for example. - Resize and/or compress thumbnails (and favicons). Often times, thumbnails are huge for the purposes of link previews. There is already support for compressing JPEGs in status-go, but I prefer to work with compression in a separate PR because I'd like to also solve the problem for PNGs (probably convert them to JPEGs, plus compress them). This would be a safe choice for thumbnails, favicons not so much because transparency is desirable. - Editing messages is not yet supported. - I haven't coded any artificial limit on the number of previews or on the size of the thumbnail payload. This will be done in a separate issue. I have heard the ideal solution may be to split messages into smaller chunks of ~125 KiB because of libp2p, but that might be too complicated at this stage of the product (?). - Link preview deletion. - For the moment, OpenGraph metadata is extracted by requesting data for the English language (and fallback to whatever is available). In the future, we'll want to unfurl by respecting the user's local device language. Some websites, like GoDaddy, are already localized based on the device's IP, but many aren't. - The website's description text should be limited by a certain number of characters, especially because it's outside our control. Exactly how much has not been decided yet, so it'll be done separately. - URL normalization can be tricky, so I implemented only the basics to help with caching. For example, the url https://status.im and HTTPS://status.im are considered identical. Also, a URL is considered valid for unfurling if its TLD exists according to publicsuffix.EffectiveTLDPlusOne. This was essential, otherwise the default Go url.Parse approach would consider many invalid URLs valid, and thus the server would waste resources trying to unfurl the unfurleable. ### Other requirements - If the message is edited, the link previews should reflect the edited text, not the original one. This has been aligned with the design team as well. - If the website's thumbnail or the favicon can't be fetched, just ignore them. The only mandatory piece of metadata is the website's title and URL. - Link previews in clients should be generated in near real-time, that is, as the user types, previews are updated. In mobile this performs very well, and it's what other clients like WhatsApp, Telegram, and Facebook do. ### Decisions - While the user typing in the input field, the client is constantly (debounced) asking status-go to parse the text and extract normalized URLs and then the client checks if they're already in its in-memory cache. If they are, no RPC call is made. I chose this approach to achieve the best possible performance in mobile and avoid the whole RPC overhead, since the chat experience is already not smooth enough. The mobile client uses URLs as cache keys in a hashmap, i.e. if the key is present, it means the preview is readily available (naive, but good enough for now). This decision also gave me more flexibility to find the best UX at this stage of the feature. - Due to the requirement that users should be able to see independent loading indicators for each link preview, when status-go can't unfurl a URL, it doesn't return it in the response. - As an initial implementation, I added the BLOB column unfurled_links to the user_messages table. The preview data is then serialized as JSON before being stored in this column. I felt that creating a separate table and the related code for this initial PR would be inconvenient. Is that reasonable to you? Once things stabilize I can create a proper table if we want to avoid this kind of solution with serialized columns.
2023-05-18 18:43:06 +00:00
}
return ""
}
func (m *UnfurledLink) GetTitle() string {
if m != nil {
return m.Title
URL unfurling (initial implementation) (#3471) This is the initial implementation for the new URL unfurling requirements. The most important one is that only the message sender will pay the privacy cost for unfurling and extracting metadata from websites. Once the message is sent, the unfurled data will be stored at the protocol level and receivers will just profit and happily decode the metadata to render it. Further development of this URL unfurling capability will be mostly guided by issues created on clients. For the moment in status-mobile: https://github.com/status-im/status-mobile/labels/url-preview - https://github.com/status-im/status-mobile/issues/15918 - https://github.com/status-im/status-mobile/issues/15917 - https://github.com/status-im/status-mobile/issues/15910 - https://github.com/status-im/status-mobile/issues/15909 - https://github.com/status-im/status-mobile/issues/15908 - https://github.com/status-im/status-mobile/issues/15906 - https://github.com/status-im/status-mobile/issues/15905 ### Terminology In the code, I've tried to stick to the word "unfurl URL" to really mean the process of extracting metadata from a website, sort of lower level. I use "link preview" to mean a higher level structure which is enriched by unfurled data. "link preview" is also how designers refer to it. ### User flows 1. Carol needs to see link previews while typing in the chat input field. Notice from the diagram nothing is persisted and that status-go endpoints are essentially stateless. ``` #+begin_src plantuml :results verbatim Client->>Server: Call wakuext_getTextURLs Server-->>Client: Normalized URLs Client->>Client: Render cached unfurled URLs Client->>Server: Unfurl non-cached URLs.\nCall wakuext_unfurlURLs Server->>Website: Fetch metadata Website-->>Server: Metadata (thumbnail URL, title, etc) Server->>Website: Fetch thumbnail Server->>Website: Fetch favicon Website-->>Server: Favicon bytes Website-->>Server: Thumbnail bytes Server->>Server: Decode & process images Server-->>Client: Unfurled data (thumbnail data URI, etc) #+end_src ``` ``` ,------. ,------. ,-------. |Client| |Server| |Website| `--+---' `--+---' `---+---' | Call wakuext_getTextURLs | | | ---------------------------------------> | | | | | Normalized URLs | | | <- - - - - - - - - - - - - - - - - - - - | | | | |----. | | | | Render cached unfurled URLs | | |<---' | | | | | | Unfurl non-cached URLs. | | | Call wakuext_unfurlURLs | | | ---------------------------------------> | | | | | | Fetch metadata | | | ------------------------------------> | | | | | Metadata (thumbnail URL, title, etc)| | | <- - - - - - - - - - - - - - - - - - | | | | | Fetch thumbnail | | | ------------------------------------> | | | | | Fetch favicon | | | ------------------------------------> | | | | | Favicon bytes | | | <- - - - - - - - - - - - - - - - - - | | | | | Thumbnail bytes | | | <- - - - - - - - - - - - - - - - - - | | | | |----. | | | | Decode & process images | | |<---' | | | | | Unfurled data (thumbnail data URI, etc)| | | <- - - - - - - - - - - - - - - - - - - - | ,--+---. ,--+---. ,---+---. |Client| |Server| |Website| `------' `------' `-------' ``` 2. Carol sends the text message with link previews in the RPC request wakuext_sendChatMessages. status-go assumes the link previews are good because it can't and shouldn't attempt to re-unfurl them. ``` #+begin_src plantuml :results verbatim Client->>Server: Call wakuext_sendChatMessages Server->>Server: Transform link previews to\nbe proto-marshalled Server->DB: Write link previews serialized as JSON Server-->>Client: Updated message response #+end_src ``` ``` ,------. ,------. ,--. |Client| |Server| |DB| `--+---' `--+---' `+-' | Call wakuext_sendChatMessages| | | -----------------------------> | | | | | |----. | | | | Transform link previews to | | |<---' be proto-marshalled | | | | | | | | | Write link previews serialized as JSON| | | --------------------------------------> | | | | Updated message response | | | <- - - - - - - - - - - - - - - | ,--+---. ,--+---. ,+-. |Client| |Server| |DB| `------' `------' `--' ``` 3. The message was sent over waku and persisted locally in Carol's device. She should now see the link previews in the chat history. There can be many link previews shared by other chat members, therefore it is important to serve the assets via the media server to avoid overloading the ReactNative bridge with lots of big JSON payloads containing base64 encoded data URIs (maybe this concern is meaningless for desktop). When a client is rendering messages with link previews, they will have the field linkPreviews, and the thumbnail URL will point to the local media server. ``` #+begin_src plantuml :results verbatim Client->>Server: GET /link-preview/thumbnail (media server) Server->>DB: Read from user_messages.unfurled_links Server->Server: Unmarshal JSON Server-->>Client: HTTP Content-Type: image/jpeg/etc #+end_src ``` ``` ,------. ,------. ,--. |Client| |Server| |DB| `--+---' `--+---' `+-' | GET /link-preview/thumbnail (media server)| | | ------------------------------------------> | | | | | | Read from user_messages.unfurled_links| | | --------------------------------------> | | | | |----. | | | | Unmarshal JSON | | |<---' | | | | | HTTP Content-Type: image/jpeg/etc | | | <- - - - - - - - - - - - - - - - - - - - - | ,--+---. ,--+---. ,+-. |Client| |Server| |DB| `------' `------' `--' ``` ### Some limitations of the current implementation The following points will become separate issues in status-go that I'll work on over the next couple weeks. In no order of importance: - Improve how multiple links are fetched; retries on failure and testing how unfurling behaves around the timeout limits (deterministically, not by making real HTTP calls as I did). https://github.com/status-im/status-go/issues/3498 - Unfurl favicons and store them in the protobuf too. - For this PR, I added unfurling support only for websites with OpenGraph https://ogp.me/ meta tags. Other unfurlers will be implemented on demand. The next one will probably be for oEmbed https://oembed.com/, the protocol supported by YouTube, for example. - Resize and/or compress thumbnails (and favicons). Often times, thumbnails are huge for the purposes of link previews. There is already support for compressing JPEGs in status-go, but I prefer to work with compression in a separate PR because I'd like to also solve the problem for PNGs (probably convert them to JPEGs, plus compress them). This would be a safe choice for thumbnails, favicons not so much because transparency is desirable. - Editing messages is not yet supported. - I haven't coded any artificial limit on the number of previews or on the size of the thumbnail payload. This will be done in a separate issue. I have heard the ideal solution may be to split messages into smaller chunks of ~125 KiB because of libp2p, but that might be too complicated at this stage of the product (?). - Link preview deletion. - For the moment, OpenGraph metadata is extracted by requesting data for the English language (and fallback to whatever is available). In the future, we'll want to unfurl by respecting the user's local device language. Some websites, like GoDaddy, are already localized based on the device's IP, but many aren't. - The website's description text should be limited by a certain number of characters, especially because it's outside our control. Exactly how much has not been decided yet, so it'll be done separately. - URL normalization can be tricky, so I implemented only the basics to help with caching. For example, the url https://status.im and HTTPS://status.im are considered identical. Also, a URL is considered valid for unfurling if its TLD exists according to publicsuffix.EffectiveTLDPlusOne. This was essential, otherwise the default Go url.Parse approach would consider many invalid URLs valid, and thus the server would waste resources trying to unfurl the unfurleable. ### Other requirements - If the message is edited, the link previews should reflect the edited text, not the original one. This has been aligned with the design team as well. - If the website's thumbnail or the favicon can't be fetched, just ignore them. The only mandatory piece of metadata is the website's title and URL. - Link previews in clients should be generated in near real-time, that is, as the user types, previews are updated. In mobile this performs very well, and it's what other clients like WhatsApp, Telegram, and Facebook do. ### Decisions - While the user typing in the input field, the client is constantly (debounced) asking status-go to parse the text and extract normalized URLs and then the client checks if they're already in its in-memory cache. If they are, no RPC call is made. I chose this approach to achieve the best possible performance in mobile and avoid the whole RPC overhead, since the chat experience is already not smooth enough. The mobile client uses URLs as cache keys in a hashmap, i.e. if the key is present, it means the preview is readily available (naive, but good enough for now). This decision also gave me more flexibility to find the best UX at this stage of the feature. - Due to the requirement that users should be able to see independent loading indicators for each link preview, when status-go can't unfurl a URL, it doesn't return it in the response. - As an initial implementation, I added the BLOB column unfurled_links to the user_messages table. The preview data is then serialized as JSON before being stored in this column. I felt that creating a separate table and the related code for this initial PR would be inconvenient. Is that reasonable to you? Once things stabilize I can create a proper table if we want to avoid this kind of solution with serialized columns.
2023-05-18 18:43:06 +00:00
}
return ""
}
func (m *UnfurledLink) GetDescription() string {
if m != nil {
return m.Description
URL unfurling (initial implementation) (#3471) This is the initial implementation for the new URL unfurling requirements. The most important one is that only the message sender will pay the privacy cost for unfurling and extracting metadata from websites. Once the message is sent, the unfurled data will be stored at the protocol level and receivers will just profit and happily decode the metadata to render it. Further development of this URL unfurling capability will be mostly guided by issues created on clients. For the moment in status-mobile: https://github.com/status-im/status-mobile/labels/url-preview - https://github.com/status-im/status-mobile/issues/15918 - https://github.com/status-im/status-mobile/issues/15917 - https://github.com/status-im/status-mobile/issues/15910 - https://github.com/status-im/status-mobile/issues/15909 - https://github.com/status-im/status-mobile/issues/15908 - https://github.com/status-im/status-mobile/issues/15906 - https://github.com/status-im/status-mobile/issues/15905 ### Terminology In the code, I've tried to stick to the word "unfurl URL" to really mean the process of extracting metadata from a website, sort of lower level. I use "link preview" to mean a higher level structure which is enriched by unfurled data. "link preview" is also how designers refer to it. ### User flows 1. Carol needs to see link previews while typing in the chat input field. Notice from the diagram nothing is persisted and that status-go endpoints are essentially stateless. ``` #+begin_src plantuml :results verbatim Client->>Server: Call wakuext_getTextURLs Server-->>Client: Normalized URLs Client->>Client: Render cached unfurled URLs Client->>Server: Unfurl non-cached URLs.\nCall wakuext_unfurlURLs Server->>Website: Fetch metadata Website-->>Server: Metadata (thumbnail URL, title, etc) Server->>Website: Fetch thumbnail Server->>Website: Fetch favicon Website-->>Server: Favicon bytes Website-->>Server: Thumbnail bytes Server->>Server: Decode & process images Server-->>Client: Unfurled data (thumbnail data URI, etc) #+end_src ``` ``` ,------. ,------. ,-------. |Client| |Server| |Website| `--+---' `--+---' `---+---' | Call wakuext_getTextURLs | | | ---------------------------------------> | | | | | Normalized URLs | | | <- - - - - - - - - - - - - - - - - - - - | | | | |----. | | | | Render cached unfurled URLs | | |<---' | | | | | | Unfurl non-cached URLs. | | | Call wakuext_unfurlURLs | | | ---------------------------------------> | | | | | | Fetch metadata | | | ------------------------------------> | | | | | Metadata (thumbnail URL, title, etc)| | | <- - - - - - - - - - - - - - - - - - | | | | | Fetch thumbnail | | | ------------------------------------> | | | | | Fetch favicon | | | ------------------------------------> | | | | | Favicon bytes | | | <- - - - - - - - - - - - - - - - - - | | | | | Thumbnail bytes | | | <- - - - - - - - - - - - - - - - - - | | | | |----. | | | | Decode & process images | | |<---' | | | | | Unfurled data (thumbnail data URI, etc)| | | <- - - - - - - - - - - - - - - - - - - - | ,--+---. ,--+---. ,---+---. |Client| |Server| |Website| `------' `------' `-------' ``` 2. Carol sends the text message with link previews in the RPC request wakuext_sendChatMessages. status-go assumes the link previews are good because it can't and shouldn't attempt to re-unfurl them. ``` #+begin_src plantuml :results verbatim Client->>Server: Call wakuext_sendChatMessages Server->>Server: Transform link previews to\nbe proto-marshalled Server->DB: Write link previews serialized as JSON Server-->>Client: Updated message response #+end_src ``` ``` ,------. ,------. ,--. |Client| |Server| |DB| `--+---' `--+---' `+-' | Call wakuext_sendChatMessages| | | -----------------------------> | | | | | |----. | | | | Transform link previews to | | |<---' be proto-marshalled | | | | | | | | | Write link previews serialized as JSON| | | --------------------------------------> | | | | Updated message response | | | <- - - - - - - - - - - - - - - | ,--+---. ,--+---. ,+-. |Client| |Server| |DB| `------' `------' `--' ``` 3. The message was sent over waku and persisted locally in Carol's device. She should now see the link previews in the chat history. There can be many link previews shared by other chat members, therefore it is important to serve the assets via the media server to avoid overloading the ReactNative bridge with lots of big JSON payloads containing base64 encoded data URIs (maybe this concern is meaningless for desktop). When a client is rendering messages with link previews, they will have the field linkPreviews, and the thumbnail URL will point to the local media server. ``` #+begin_src plantuml :results verbatim Client->>Server: GET /link-preview/thumbnail (media server) Server->>DB: Read from user_messages.unfurled_links Server->Server: Unmarshal JSON Server-->>Client: HTTP Content-Type: image/jpeg/etc #+end_src ``` ``` ,------. ,------. ,--. |Client| |Server| |DB| `--+---' `--+---' `+-' | GET /link-preview/thumbnail (media server)| | | ------------------------------------------> | | | | | | Read from user_messages.unfurled_links| | | --------------------------------------> | | | | |----. | | | | Unmarshal JSON | | |<---' | | | | | HTTP Content-Type: image/jpeg/etc | | | <- - - - - - - - - - - - - - - - - - - - - | ,--+---. ,--+---. ,+-. |Client| |Server| |DB| `------' `------' `--' ``` ### Some limitations of the current implementation The following points will become separate issues in status-go that I'll work on over the next couple weeks. In no order of importance: - Improve how multiple links are fetched; retries on failure and testing how unfurling behaves around the timeout limits (deterministically, not by making real HTTP calls as I did). https://github.com/status-im/status-go/issues/3498 - Unfurl favicons and store them in the protobuf too. - For this PR, I added unfurling support only for websites with OpenGraph https://ogp.me/ meta tags. Other unfurlers will be implemented on demand. The next one will probably be for oEmbed https://oembed.com/, the protocol supported by YouTube, for example. - Resize and/or compress thumbnails (and favicons). Often times, thumbnails are huge for the purposes of link previews. There is already support for compressing JPEGs in status-go, but I prefer to work with compression in a separate PR because I'd like to also solve the problem for PNGs (probably convert them to JPEGs, plus compress them). This would be a safe choice for thumbnails, favicons not so much because transparency is desirable. - Editing messages is not yet supported. - I haven't coded any artificial limit on the number of previews or on the size of the thumbnail payload. This will be done in a separate issue. I have heard the ideal solution may be to split messages into smaller chunks of ~125 KiB because of libp2p, but that might be too complicated at this stage of the product (?). - Link preview deletion. - For the moment, OpenGraph metadata is extracted by requesting data for the English language (and fallback to whatever is available). In the future, we'll want to unfurl by respecting the user's local device language. Some websites, like GoDaddy, are already localized based on the device's IP, but many aren't. - The website's description text should be limited by a certain number of characters, especially because it's outside our control. Exactly how much has not been decided yet, so it'll be done separately. - URL normalization can be tricky, so I implemented only the basics to help with caching. For example, the url https://status.im and HTTPS://status.im are considered identical. Also, a URL is considered valid for unfurling if its TLD exists according to publicsuffix.EffectiveTLDPlusOne. This was essential, otherwise the default Go url.Parse approach would consider many invalid URLs valid, and thus the server would waste resources trying to unfurl the unfurleable. ### Other requirements - If the message is edited, the link previews should reflect the edited text, not the original one. This has been aligned with the design team as well. - If the website's thumbnail or the favicon can't be fetched, just ignore them. The only mandatory piece of metadata is the website's title and URL. - Link previews in clients should be generated in near real-time, that is, as the user types, previews are updated. In mobile this performs very well, and it's what other clients like WhatsApp, Telegram, and Facebook do. ### Decisions - While the user typing in the input field, the client is constantly (debounced) asking status-go to parse the text and extract normalized URLs and then the client checks if they're already in its in-memory cache. If they are, no RPC call is made. I chose this approach to achieve the best possible performance in mobile and avoid the whole RPC overhead, since the chat experience is already not smooth enough. The mobile client uses URLs as cache keys in a hashmap, i.e. if the key is present, it means the preview is readily available (naive, but good enough for now). This decision also gave me more flexibility to find the best UX at this stage of the feature. - Due to the requirement that users should be able to see independent loading indicators for each link preview, when status-go can't unfurl a URL, it doesn't return it in the response. - As an initial implementation, I added the BLOB column unfurled_links to the user_messages table. The preview data is then serialized as JSON before being stored in this column. I felt that creating a separate table and the related code for this initial PR would be inconvenient. Is that reasonable to you? Once things stabilize I can create a proper table if we want to avoid this kind of solution with serialized columns.
2023-05-18 18:43:06 +00:00
}
return ""
}
func (m *UnfurledLink) GetThumbnailPayload() []byte {
if m != nil {
return m.ThumbnailPayload
URL unfurling (initial implementation) (#3471) This is the initial implementation for the new URL unfurling requirements. The most important one is that only the message sender will pay the privacy cost for unfurling and extracting metadata from websites. Once the message is sent, the unfurled data will be stored at the protocol level and receivers will just profit and happily decode the metadata to render it. Further development of this URL unfurling capability will be mostly guided by issues created on clients. For the moment in status-mobile: https://github.com/status-im/status-mobile/labels/url-preview - https://github.com/status-im/status-mobile/issues/15918 - https://github.com/status-im/status-mobile/issues/15917 - https://github.com/status-im/status-mobile/issues/15910 - https://github.com/status-im/status-mobile/issues/15909 - https://github.com/status-im/status-mobile/issues/15908 - https://github.com/status-im/status-mobile/issues/15906 - https://github.com/status-im/status-mobile/issues/15905 ### Terminology In the code, I've tried to stick to the word "unfurl URL" to really mean the process of extracting metadata from a website, sort of lower level. I use "link preview" to mean a higher level structure which is enriched by unfurled data. "link preview" is also how designers refer to it. ### User flows 1. Carol needs to see link previews while typing in the chat input field. Notice from the diagram nothing is persisted and that status-go endpoints are essentially stateless. ``` #+begin_src plantuml :results verbatim Client->>Server: Call wakuext_getTextURLs Server-->>Client: Normalized URLs Client->>Client: Render cached unfurled URLs Client->>Server: Unfurl non-cached URLs.\nCall wakuext_unfurlURLs Server->>Website: Fetch metadata Website-->>Server: Metadata (thumbnail URL, title, etc) Server->>Website: Fetch thumbnail Server->>Website: Fetch favicon Website-->>Server: Favicon bytes Website-->>Server: Thumbnail bytes Server->>Server: Decode & process images Server-->>Client: Unfurled data (thumbnail data URI, etc) #+end_src ``` ``` ,------. ,------. ,-------. |Client| |Server| |Website| `--+---' `--+---' `---+---' | Call wakuext_getTextURLs | | | ---------------------------------------> | | | | | Normalized URLs | | | <- - - - - - - - - - - - - - - - - - - - | | | | |----. | | | | Render cached unfurled URLs | | |<---' | | | | | | Unfurl non-cached URLs. | | | Call wakuext_unfurlURLs | | | ---------------------------------------> | | | | | | Fetch metadata | | | ------------------------------------> | | | | | Metadata (thumbnail URL, title, etc)| | | <- - - - - - - - - - - - - - - - - - | | | | | Fetch thumbnail | | | ------------------------------------> | | | | | Fetch favicon | | | ------------------------------------> | | | | | Favicon bytes | | | <- - - - - - - - - - - - - - - - - - | | | | | Thumbnail bytes | | | <- - - - - - - - - - - - - - - - - - | | | | |----. | | | | Decode & process images | | |<---' | | | | | Unfurled data (thumbnail data URI, etc)| | | <- - - - - - - - - - - - - - - - - - - - | ,--+---. ,--+---. ,---+---. |Client| |Server| |Website| `------' `------' `-------' ``` 2. Carol sends the text message with link previews in the RPC request wakuext_sendChatMessages. status-go assumes the link previews are good because it can't and shouldn't attempt to re-unfurl them. ``` #+begin_src plantuml :results verbatim Client->>Server: Call wakuext_sendChatMessages Server->>Server: Transform link previews to\nbe proto-marshalled Server->DB: Write link previews serialized as JSON Server-->>Client: Updated message response #+end_src ``` ``` ,------. ,------. ,--. |Client| |Server| |DB| `--+---' `--+---' `+-' | Call wakuext_sendChatMessages| | | -----------------------------> | | | | | |----. | | | | Transform link previews to | | |<---' be proto-marshalled | | | | | | | | | Write link previews serialized as JSON| | | --------------------------------------> | | | | Updated message response | | | <- - - - - - - - - - - - - - - | ,--+---. ,--+---. ,+-. |Client| |Server| |DB| `------' `------' `--' ``` 3. The message was sent over waku and persisted locally in Carol's device. She should now see the link previews in the chat history. There can be many link previews shared by other chat members, therefore it is important to serve the assets via the media server to avoid overloading the ReactNative bridge with lots of big JSON payloads containing base64 encoded data URIs (maybe this concern is meaningless for desktop). When a client is rendering messages with link previews, they will have the field linkPreviews, and the thumbnail URL will point to the local media server. ``` #+begin_src plantuml :results verbatim Client->>Server: GET /link-preview/thumbnail (media server) Server->>DB: Read from user_messages.unfurled_links Server->Server: Unmarshal JSON Server-->>Client: HTTP Content-Type: image/jpeg/etc #+end_src ``` ``` ,------. ,------. ,--. |Client| |Server| |DB| `--+---' `--+---' `+-' | GET /link-preview/thumbnail (media server)| | | ------------------------------------------> | | | | | | Read from user_messages.unfurled_links| | | --------------------------------------> | | | | |----. | | | | Unmarshal JSON | | |<---' | | | | | HTTP Content-Type: image/jpeg/etc | | | <- - - - - - - - - - - - - - - - - - - - - | ,--+---. ,--+---. ,+-. |Client| |Server| |DB| `------' `------' `--' ``` ### Some limitations of the current implementation The following points will become separate issues in status-go that I'll work on over the next couple weeks. In no order of importance: - Improve how multiple links are fetched; retries on failure and testing how unfurling behaves around the timeout limits (deterministically, not by making real HTTP calls as I did). https://github.com/status-im/status-go/issues/3498 - Unfurl favicons and store them in the protobuf too. - For this PR, I added unfurling support only for websites with OpenGraph https://ogp.me/ meta tags. Other unfurlers will be implemented on demand. The next one will probably be for oEmbed https://oembed.com/, the protocol supported by YouTube, for example. - Resize and/or compress thumbnails (and favicons). Often times, thumbnails are huge for the purposes of link previews. There is already support for compressing JPEGs in status-go, but I prefer to work with compression in a separate PR because I'd like to also solve the problem for PNGs (probably convert them to JPEGs, plus compress them). This would be a safe choice for thumbnails, favicons not so much because transparency is desirable. - Editing messages is not yet supported. - I haven't coded any artificial limit on the number of previews or on the size of the thumbnail payload. This will be done in a separate issue. I have heard the ideal solution may be to split messages into smaller chunks of ~125 KiB because of libp2p, but that might be too complicated at this stage of the product (?). - Link preview deletion. - For the moment, OpenGraph metadata is extracted by requesting data for the English language (and fallback to whatever is available). In the future, we'll want to unfurl by respecting the user's local device language. Some websites, like GoDaddy, are already localized based on the device's IP, but many aren't. - The website's description text should be limited by a certain number of characters, especially because it's outside our control. Exactly how much has not been decided yet, so it'll be done separately. - URL normalization can be tricky, so I implemented only the basics to help with caching. For example, the url https://status.im and HTTPS://status.im are considered identical. Also, a URL is considered valid for unfurling if its TLD exists according to publicsuffix.EffectiveTLDPlusOne. This was essential, otherwise the default Go url.Parse approach would consider many invalid URLs valid, and thus the server would waste resources trying to unfurl the unfurleable. ### Other requirements - If the message is edited, the link previews should reflect the edited text, not the original one. This has been aligned with the design team as well. - If the website's thumbnail or the favicon can't be fetched, just ignore them. The only mandatory piece of metadata is the website's title and URL. - Link previews in clients should be generated in near real-time, that is, as the user types, previews are updated. In mobile this performs very well, and it's what other clients like WhatsApp, Telegram, and Facebook do. ### Decisions - While the user typing in the input field, the client is constantly (debounced) asking status-go to parse the text and extract normalized URLs and then the client checks if they're already in its in-memory cache. If they are, no RPC call is made. I chose this approach to achieve the best possible performance in mobile and avoid the whole RPC overhead, since the chat experience is already not smooth enough. The mobile client uses URLs as cache keys in a hashmap, i.e. if the key is present, it means the preview is readily available (naive, but good enough for now). This decision also gave me more flexibility to find the best UX at this stage of the feature. - Due to the requirement that users should be able to see independent loading indicators for each link preview, when status-go can't unfurl a URL, it doesn't return it in the response. - As an initial implementation, I added the BLOB column unfurled_links to the user_messages table. The preview data is then serialized as JSON before being stored in this column. I felt that creating a separate table and the related code for this initial PR would be inconvenient. Is that reasonable to you? Once things stabilize I can create a proper table if we want to avoid this kind of solution with serialized columns.
2023-05-18 18:43:06 +00:00
}
return nil
}
func (m *UnfurledLink) GetThumbnailWidth() uint32 {
if m != nil {
return m.ThumbnailWidth
URL unfurling (initial implementation) (#3471) This is the initial implementation for the new URL unfurling requirements. The most important one is that only the message sender will pay the privacy cost for unfurling and extracting metadata from websites. Once the message is sent, the unfurled data will be stored at the protocol level and receivers will just profit and happily decode the metadata to render it. Further development of this URL unfurling capability will be mostly guided by issues created on clients. For the moment in status-mobile: https://github.com/status-im/status-mobile/labels/url-preview - https://github.com/status-im/status-mobile/issues/15918 - https://github.com/status-im/status-mobile/issues/15917 - https://github.com/status-im/status-mobile/issues/15910 - https://github.com/status-im/status-mobile/issues/15909 - https://github.com/status-im/status-mobile/issues/15908 - https://github.com/status-im/status-mobile/issues/15906 - https://github.com/status-im/status-mobile/issues/15905 ### Terminology In the code, I've tried to stick to the word "unfurl URL" to really mean the process of extracting metadata from a website, sort of lower level. I use "link preview" to mean a higher level structure which is enriched by unfurled data. "link preview" is also how designers refer to it. ### User flows 1. Carol needs to see link previews while typing in the chat input field. Notice from the diagram nothing is persisted and that status-go endpoints are essentially stateless. ``` #+begin_src plantuml :results verbatim Client->>Server: Call wakuext_getTextURLs Server-->>Client: Normalized URLs Client->>Client: Render cached unfurled URLs Client->>Server: Unfurl non-cached URLs.\nCall wakuext_unfurlURLs Server->>Website: Fetch metadata Website-->>Server: Metadata (thumbnail URL, title, etc) Server->>Website: Fetch thumbnail Server->>Website: Fetch favicon Website-->>Server: Favicon bytes Website-->>Server: Thumbnail bytes Server->>Server: Decode & process images Server-->>Client: Unfurled data (thumbnail data URI, etc) #+end_src ``` ``` ,------. ,------. ,-------. |Client| |Server| |Website| `--+---' `--+---' `---+---' | Call wakuext_getTextURLs | | | ---------------------------------------> | | | | | Normalized URLs | | | <- - - - - - - - - - - - - - - - - - - - | | | | |----. | | | | Render cached unfurled URLs | | |<---' | | | | | | Unfurl non-cached URLs. | | | Call wakuext_unfurlURLs | | | ---------------------------------------> | | | | | | Fetch metadata | | | ------------------------------------> | | | | | Metadata (thumbnail URL, title, etc)| | | <- - - - - - - - - - - - - - - - - - | | | | | Fetch thumbnail | | | ------------------------------------> | | | | | Fetch favicon | | | ------------------------------------> | | | | | Favicon bytes | | | <- - - - - - - - - - - - - - - - - - | | | | | Thumbnail bytes | | | <- - - - - - - - - - - - - - - - - - | | | | |----. | | | | Decode & process images | | |<---' | | | | | Unfurled data (thumbnail data URI, etc)| | | <- - - - - - - - - - - - - - - - - - - - | ,--+---. ,--+---. ,---+---. |Client| |Server| |Website| `------' `------' `-------' ``` 2. Carol sends the text message with link previews in the RPC request wakuext_sendChatMessages. status-go assumes the link previews are good because it can't and shouldn't attempt to re-unfurl them. ``` #+begin_src plantuml :results verbatim Client->>Server: Call wakuext_sendChatMessages Server->>Server: Transform link previews to\nbe proto-marshalled Server->DB: Write link previews serialized as JSON Server-->>Client: Updated message response #+end_src ``` ``` ,------. ,------. ,--. |Client| |Server| |DB| `--+---' `--+---' `+-' | Call wakuext_sendChatMessages| | | -----------------------------> | | | | | |----. | | | | Transform link previews to | | |<---' be proto-marshalled | | | | | | | | | Write link previews serialized as JSON| | | --------------------------------------> | | | | Updated message response | | | <- - - - - - - - - - - - - - - | ,--+---. ,--+---. ,+-. |Client| |Server| |DB| `------' `------' `--' ``` 3. The message was sent over waku and persisted locally in Carol's device. She should now see the link previews in the chat history. There can be many link previews shared by other chat members, therefore it is important to serve the assets via the media server to avoid overloading the ReactNative bridge with lots of big JSON payloads containing base64 encoded data URIs (maybe this concern is meaningless for desktop). When a client is rendering messages with link previews, they will have the field linkPreviews, and the thumbnail URL will point to the local media server. ``` #+begin_src plantuml :results verbatim Client->>Server: GET /link-preview/thumbnail (media server) Server->>DB: Read from user_messages.unfurled_links Server->Server: Unmarshal JSON Server-->>Client: HTTP Content-Type: image/jpeg/etc #+end_src ``` ``` ,------. ,------. ,--. |Client| |Server| |DB| `--+---' `--+---' `+-' | GET /link-preview/thumbnail (media server)| | | ------------------------------------------> | | | | | | Read from user_messages.unfurled_links| | | --------------------------------------> | | | | |----. | | | | Unmarshal JSON | | |<---' | | | | | HTTP Content-Type: image/jpeg/etc | | | <- - - - - - - - - - - - - - - - - - - - - | ,--+---. ,--+---. ,+-. |Client| |Server| |DB| `------' `------' `--' ``` ### Some limitations of the current implementation The following points will become separate issues in status-go that I'll work on over the next couple weeks. In no order of importance: - Improve how multiple links are fetched; retries on failure and testing how unfurling behaves around the timeout limits (deterministically, not by making real HTTP calls as I did). https://github.com/status-im/status-go/issues/3498 - Unfurl favicons and store them in the protobuf too. - For this PR, I added unfurling support only for websites with OpenGraph https://ogp.me/ meta tags. Other unfurlers will be implemented on demand. The next one will probably be for oEmbed https://oembed.com/, the protocol supported by YouTube, for example. - Resize and/or compress thumbnails (and favicons). Often times, thumbnails are huge for the purposes of link previews. There is already support for compressing JPEGs in status-go, but I prefer to work with compression in a separate PR because I'd like to also solve the problem for PNGs (probably convert them to JPEGs, plus compress them). This would be a safe choice for thumbnails, favicons not so much because transparency is desirable. - Editing messages is not yet supported. - I haven't coded any artificial limit on the number of previews or on the size of the thumbnail payload. This will be done in a separate issue. I have heard the ideal solution may be to split messages into smaller chunks of ~125 KiB because of libp2p, but that might be too complicated at this stage of the product (?). - Link preview deletion. - For the moment, OpenGraph metadata is extracted by requesting data for the English language (and fallback to whatever is available). In the future, we'll want to unfurl by respecting the user's local device language. Some websites, like GoDaddy, are already localized based on the device's IP, but many aren't. - The website's description text should be limited by a certain number of characters, especially because it's outside our control. Exactly how much has not been decided yet, so it'll be done separately. - URL normalization can be tricky, so I implemented only the basics to help with caching. For example, the url https://status.im and HTTPS://status.im are considered identical. Also, a URL is considered valid for unfurling if its TLD exists according to publicsuffix.EffectiveTLDPlusOne. This was essential, otherwise the default Go url.Parse approach would consider many invalid URLs valid, and thus the server would waste resources trying to unfurl the unfurleable. ### Other requirements - If the message is edited, the link previews should reflect the edited text, not the original one. This has been aligned with the design team as well. - If the website's thumbnail or the favicon can't be fetched, just ignore them. The only mandatory piece of metadata is the website's title and URL. - Link previews in clients should be generated in near real-time, that is, as the user types, previews are updated. In mobile this performs very well, and it's what other clients like WhatsApp, Telegram, and Facebook do. ### Decisions - While the user typing in the input field, the client is constantly (debounced) asking status-go to parse the text and extract normalized URLs and then the client checks if they're already in its in-memory cache. If they are, no RPC call is made. I chose this approach to achieve the best possible performance in mobile and avoid the whole RPC overhead, since the chat experience is already not smooth enough. The mobile client uses URLs as cache keys in a hashmap, i.e. if the key is present, it means the preview is readily available (naive, but good enough for now). This decision also gave me more flexibility to find the best UX at this stage of the feature. - Due to the requirement that users should be able to see independent loading indicators for each link preview, when status-go can't unfurl a URL, it doesn't return it in the response. - As an initial implementation, I added the BLOB column unfurled_links to the user_messages table. The preview data is then serialized as JSON before being stored in this column. I felt that creating a separate table and the related code for this initial PR would be inconvenient. Is that reasonable to you? Once things stabilize I can create a proper table if we want to avoid this kind of solution with serialized columns.
2023-05-18 18:43:06 +00:00
}
return 0
}
func (m *UnfurledLink) GetThumbnailHeight() uint32 {
if m != nil {
return m.ThumbnailHeight
URL unfurling (initial implementation) (#3471) This is the initial implementation for the new URL unfurling requirements. The most important one is that only the message sender will pay the privacy cost for unfurling and extracting metadata from websites. Once the message is sent, the unfurled data will be stored at the protocol level and receivers will just profit and happily decode the metadata to render it. Further development of this URL unfurling capability will be mostly guided by issues created on clients. For the moment in status-mobile: https://github.com/status-im/status-mobile/labels/url-preview - https://github.com/status-im/status-mobile/issues/15918 - https://github.com/status-im/status-mobile/issues/15917 - https://github.com/status-im/status-mobile/issues/15910 - https://github.com/status-im/status-mobile/issues/15909 - https://github.com/status-im/status-mobile/issues/15908 - https://github.com/status-im/status-mobile/issues/15906 - https://github.com/status-im/status-mobile/issues/15905 ### Terminology In the code, I've tried to stick to the word "unfurl URL" to really mean the process of extracting metadata from a website, sort of lower level. I use "link preview" to mean a higher level structure which is enriched by unfurled data. "link preview" is also how designers refer to it. ### User flows 1. Carol needs to see link previews while typing in the chat input field. Notice from the diagram nothing is persisted and that status-go endpoints are essentially stateless. ``` #+begin_src plantuml :results verbatim Client->>Server: Call wakuext_getTextURLs Server-->>Client: Normalized URLs Client->>Client: Render cached unfurled URLs Client->>Server: Unfurl non-cached URLs.\nCall wakuext_unfurlURLs Server->>Website: Fetch metadata Website-->>Server: Metadata (thumbnail URL, title, etc) Server->>Website: Fetch thumbnail Server->>Website: Fetch favicon Website-->>Server: Favicon bytes Website-->>Server: Thumbnail bytes Server->>Server: Decode & process images Server-->>Client: Unfurled data (thumbnail data URI, etc) #+end_src ``` ``` ,------. ,------. ,-------. |Client| |Server| |Website| `--+---' `--+---' `---+---' | Call wakuext_getTextURLs | | | ---------------------------------------> | | | | | Normalized URLs | | | <- - - - - - - - - - - - - - - - - - - - | | | | |----. | | | | Render cached unfurled URLs | | |<---' | | | | | | Unfurl non-cached URLs. | | | Call wakuext_unfurlURLs | | | ---------------------------------------> | | | | | | Fetch metadata | | | ------------------------------------> | | | | | Metadata (thumbnail URL, title, etc)| | | <- - - - - - - - - - - - - - - - - - | | | | | Fetch thumbnail | | | ------------------------------------> | | | | | Fetch favicon | | | ------------------------------------> | | | | | Favicon bytes | | | <- - - - - - - - - - - - - - - - - - | | | | | Thumbnail bytes | | | <- - - - - - - - - - - - - - - - - - | | | | |----. | | | | Decode & process images | | |<---' | | | | | Unfurled data (thumbnail data URI, etc)| | | <- - - - - - - - - - - - - - - - - - - - | ,--+---. ,--+---. ,---+---. |Client| |Server| |Website| `------' `------' `-------' ``` 2. Carol sends the text message with link previews in the RPC request wakuext_sendChatMessages. status-go assumes the link previews are good because it can't and shouldn't attempt to re-unfurl them. ``` #+begin_src plantuml :results verbatim Client->>Server: Call wakuext_sendChatMessages Server->>Server: Transform link previews to\nbe proto-marshalled Server->DB: Write link previews serialized as JSON Server-->>Client: Updated message response #+end_src ``` ``` ,------. ,------. ,--. |Client| |Server| |DB| `--+---' `--+---' `+-' | Call wakuext_sendChatMessages| | | -----------------------------> | | | | | |----. | | | | Transform link previews to | | |<---' be proto-marshalled | | | | | | | | | Write link previews serialized as JSON| | | --------------------------------------> | | | | Updated message response | | | <- - - - - - - - - - - - - - - | ,--+---. ,--+---. ,+-. |Client| |Server| |DB| `------' `------' `--' ``` 3. The message was sent over waku and persisted locally in Carol's device. She should now see the link previews in the chat history. There can be many link previews shared by other chat members, therefore it is important to serve the assets via the media server to avoid overloading the ReactNative bridge with lots of big JSON payloads containing base64 encoded data URIs (maybe this concern is meaningless for desktop). When a client is rendering messages with link previews, they will have the field linkPreviews, and the thumbnail URL will point to the local media server. ``` #+begin_src plantuml :results verbatim Client->>Server: GET /link-preview/thumbnail (media server) Server->>DB: Read from user_messages.unfurled_links Server->Server: Unmarshal JSON Server-->>Client: HTTP Content-Type: image/jpeg/etc #+end_src ``` ``` ,------. ,------. ,--. |Client| |Server| |DB| `--+---' `--+---' `+-' | GET /link-preview/thumbnail (media server)| | | ------------------------------------------> | | | | | | Read from user_messages.unfurled_links| | | --------------------------------------> | | | | |----. | | | | Unmarshal JSON | | |<---' | | | | | HTTP Content-Type: image/jpeg/etc | | | <- - - - - - - - - - - - - - - - - - - - - | ,--+---. ,--+---. ,+-. |Client| |Server| |DB| `------' `------' `--' ``` ### Some limitations of the current implementation The following points will become separate issues in status-go that I'll work on over the next couple weeks. In no order of importance: - Improve how multiple links are fetched; retries on failure and testing how unfurling behaves around the timeout limits (deterministically, not by making real HTTP calls as I did). https://github.com/status-im/status-go/issues/3498 - Unfurl favicons and store them in the protobuf too. - For this PR, I added unfurling support only for websites with OpenGraph https://ogp.me/ meta tags. Other unfurlers will be implemented on demand. The next one will probably be for oEmbed https://oembed.com/, the protocol supported by YouTube, for example. - Resize and/or compress thumbnails (and favicons). Often times, thumbnails are huge for the purposes of link previews. There is already support for compressing JPEGs in status-go, but I prefer to work with compression in a separate PR because I'd like to also solve the problem for PNGs (probably convert them to JPEGs, plus compress them). This would be a safe choice for thumbnails, favicons not so much because transparency is desirable. - Editing messages is not yet supported. - I haven't coded any artificial limit on the number of previews or on the size of the thumbnail payload. This will be done in a separate issue. I have heard the ideal solution may be to split messages into smaller chunks of ~125 KiB because of libp2p, but that might be too complicated at this stage of the product (?). - Link preview deletion. - For the moment, OpenGraph metadata is extracted by requesting data for the English language (and fallback to whatever is available). In the future, we'll want to unfurl by respecting the user's local device language. Some websites, like GoDaddy, are already localized based on the device's IP, but many aren't. - The website's description text should be limited by a certain number of characters, especially because it's outside our control. Exactly how much has not been decided yet, so it'll be done separately. - URL normalization can be tricky, so I implemented only the basics to help with caching. For example, the url https://status.im and HTTPS://status.im are considered identical. Also, a URL is considered valid for unfurling if its TLD exists according to publicsuffix.EffectiveTLDPlusOne. This was essential, otherwise the default Go url.Parse approach would consider many invalid URLs valid, and thus the server would waste resources trying to unfurl the unfurleable. ### Other requirements - If the message is edited, the link previews should reflect the edited text, not the original one. This has been aligned with the design team as well. - If the website's thumbnail or the favicon can't be fetched, just ignore them. The only mandatory piece of metadata is the website's title and URL. - Link previews in clients should be generated in near real-time, that is, as the user types, previews are updated. In mobile this performs very well, and it's what other clients like WhatsApp, Telegram, and Facebook do. ### Decisions - While the user typing in the input field, the client is constantly (debounced) asking status-go to parse the text and extract normalized URLs and then the client checks if they're already in its in-memory cache. If they are, no RPC call is made. I chose this approach to achieve the best possible performance in mobile and avoid the whole RPC overhead, since the chat experience is already not smooth enough. The mobile client uses URLs as cache keys in a hashmap, i.e. if the key is present, it means the preview is readily available (naive, but good enough for now). This decision also gave me more flexibility to find the best UX at this stage of the feature. - Due to the requirement that users should be able to see independent loading indicators for each link preview, when status-go can't unfurl a URL, it doesn't return it in the response. - As an initial implementation, I added the BLOB column unfurled_links to the user_messages table. The preview data is then serialized as JSON before being stored in this column. I felt that creating a separate table and the related code for this initial PR would be inconvenient. Is that reasonable to you? Once things stabilize I can create a proper table if we want to avoid this kind of solution with serialized columns.
2023-05-18 18:43:06 +00:00
}
return 0
}
func (m *UnfurledLink) GetType() UnfurledLink_LinkType {
if m != nil {
return m.Type
}
return UnfurledLink_LINK
}
2023-10-13 12:25:34 +00:00
type UnfurledStatusContactLink struct {
PublicKey []byte `protobuf:"bytes,1,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"`
DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"`
Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"`
Icon *UnfurledLinkThumbnail `protobuf:"bytes,4,opt,name=icon,proto3" json:"icon,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *UnfurledStatusContactLink) Reset() { *m = UnfurledStatusContactLink{} }
func (m *UnfurledStatusContactLink) String() string { return proto.CompactTextString(m) }
func (*UnfurledStatusContactLink) ProtoMessage() {}
func (*UnfurledStatusContactLink) Descriptor() ([]byte, []int) {
return fileDescriptor_263952f55fd35689, []int{12}
}
func (m *UnfurledStatusContactLink) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_UnfurledStatusContactLink.Unmarshal(m, b)
}
func (m *UnfurledStatusContactLink) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_UnfurledStatusContactLink.Marshal(b, m, deterministic)
}
func (m *UnfurledStatusContactLink) XXX_Merge(src proto.Message) {
xxx_messageInfo_UnfurledStatusContactLink.Merge(m, src)
}
func (m *UnfurledStatusContactLink) XXX_Size() int {
return xxx_messageInfo_UnfurledStatusContactLink.Size(m)
}
func (m *UnfurledStatusContactLink) XXX_DiscardUnknown() {
xxx_messageInfo_UnfurledStatusContactLink.DiscardUnknown(m)
}
var xxx_messageInfo_UnfurledStatusContactLink proto.InternalMessageInfo
func (m *UnfurledStatusContactLink) GetPublicKey() []byte {
if m != nil {
return m.PublicKey
2023-10-13 12:25:34 +00:00
}
return nil
}
func (m *UnfurledStatusContactLink) GetDisplayName() string {
if m != nil {
return m.DisplayName
2023-10-13 12:25:34 +00:00
}
return ""
}
func (m *UnfurledStatusContactLink) GetDescription() string {
if m != nil {
return m.Description
2023-10-13 12:25:34 +00:00
}
return ""
}
func (m *UnfurledStatusContactLink) GetIcon() *UnfurledLinkThumbnail {
if m != nil {
return m.Icon
2023-10-13 12:25:34 +00:00
}
return nil
}
type UnfurledStatusCommunityLink struct {
CommunityId []byte `protobuf:"bytes,1,opt,name=community_id,json=communityId,proto3" json:"community_id,omitempty"`
DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"`
Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"`
MembersCount uint32 `protobuf:"varint,4,opt,name=members_count,json=membersCount,proto3" json:"members_count,omitempty"`
Color string `protobuf:"bytes,5,opt,name=color,proto3" json:"color,omitempty"`
Icon *UnfurledLinkThumbnail `protobuf:"bytes,7,opt,name=icon,proto3" json:"icon,omitempty"`
Banner *UnfurledLinkThumbnail `protobuf:"bytes,8,opt,name=banner,proto3" json:"banner,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *UnfurledStatusCommunityLink) Reset() { *m = UnfurledStatusCommunityLink{} }
func (m *UnfurledStatusCommunityLink) String() string { return proto.CompactTextString(m) }
func (*UnfurledStatusCommunityLink) ProtoMessage() {}
func (*UnfurledStatusCommunityLink) Descriptor() ([]byte, []int) {
return fileDescriptor_263952f55fd35689, []int{13}
}
func (m *UnfurledStatusCommunityLink) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_UnfurledStatusCommunityLink.Unmarshal(m, b)
}
func (m *UnfurledStatusCommunityLink) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_UnfurledStatusCommunityLink.Marshal(b, m, deterministic)
}
func (m *UnfurledStatusCommunityLink) XXX_Merge(src proto.Message) {
xxx_messageInfo_UnfurledStatusCommunityLink.Merge(m, src)
}
func (m *UnfurledStatusCommunityLink) XXX_Size() int {
return xxx_messageInfo_UnfurledStatusCommunityLink.Size(m)
}
func (m *UnfurledStatusCommunityLink) XXX_DiscardUnknown() {
xxx_messageInfo_UnfurledStatusCommunityLink.DiscardUnknown(m)
}
var xxx_messageInfo_UnfurledStatusCommunityLink proto.InternalMessageInfo
2023-10-13 12:25:34 +00:00
func (m *UnfurledStatusCommunityLink) GetCommunityId() []byte {
if m != nil {
return m.CommunityId
2023-10-13 12:25:34 +00:00
}
return nil
}
func (m *UnfurledStatusCommunityLink) GetDisplayName() string {
if m != nil {
return m.DisplayName
2023-10-13 12:25:34 +00:00
}
return ""
}
func (m *UnfurledStatusCommunityLink) GetDescription() string {
if m != nil {
return m.Description
2023-10-13 12:25:34 +00:00
}
return ""
}
func (m *UnfurledStatusCommunityLink) GetMembersCount() uint32 {
if m != nil {
return m.MembersCount
2023-10-13 12:25:34 +00:00
}
return 0
}
func (m *UnfurledStatusCommunityLink) GetColor() string {
if m != nil {
return m.Color
2023-10-13 12:25:34 +00:00
}
return ""
}
func (m *UnfurledStatusCommunityLink) GetIcon() *UnfurledLinkThumbnail {
if m != nil {
return m.Icon
2023-10-13 12:25:34 +00:00
}
return nil
}
func (m *UnfurledStatusCommunityLink) GetBanner() *UnfurledLinkThumbnail {
if m != nil {
return m.Banner
2023-10-13 12:25:34 +00:00
}
return nil
}
type UnfurledStatusChannelLink struct {
ChannelUuid string `protobuf:"bytes,1,opt,name=channel_uuid,json=channelUuid,proto3" json:"channel_uuid,omitempty"`
Emoji string `protobuf:"bytes,2,opt,name=emoji,proto3" json:"emoji,omitempty"`
DisplayName string `protobuf:"bytes,3,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"`
Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"`
Color string `protobuf:"bytes,5,opt,name=color,proto3" json:"color,omitempty"`
Community *UnfurledStatusCommunityLink `protobuf:"bytes,6,opt,name=community,proto3" json:"community,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *UnfurledStatusChannelLink) Reset() { *m = UnfurledStatusChannelLink{} }
func (m *UnfurledStatusChannelLink) String() string { return proto.CompactTextString(m) }
func (*UnfurledStatusChannelLink) ProtoMessage() {}
func (*UnfurledStatusChannelLink) Descriptor() ([]byte, []int) {
return fileDescriptor_263952f55fd35689, []int{14}
}
func (m *UnfurledStatusChannelLink) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_UnfurledStatusChannelLink.Unmarshal(m, b)
}
func (m *UnfurledStatusChannelLink) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_UnfurledStatusChannelLink.Marshal(b, m, deterministic)
}
func (m *UnfurledStatusChannelLink) XXX_Merge(src proto.Message) {
xxx_messageInfo_UnfurledStatusChannelLink.Merge(m, src)
}
func (m *UnfurledStatusChannelLink) XXX_Size() int {
return xxx_messageInfo_UnfurledStatusChannelLink.Size(m)
}
func (m *UnfurledStatusChannelLink) XXX_DiscardUnknown() {
xxx_messageInfo_UnfurledStatusChannelLink.DiscardUnknown(m)
}
var xxx_messageInfo_UnfurledStatusChannelLink proto.InternalMessageInfo
func (m *UnfurledStatusChannelLink) GetChannelUuid() string {
if m != nil {
return m.ChannelUuid
2023-10-13 12:25:34 +00:00
}
2023-11-09 16:24:35 +00:00
return ""
2023-10-13 12:25:34 +00:00
}
func (m *UnfurledStatusChannelLink) GetEmoji() string {
if m != nil {
return m.Emoji
2023-10-13 12:25:34 +00:00
}
return ""
}
func (m *UnfurledStatusChannelLink) GetDisplayName() string {
if m != nil {
return m.DisplayName
2023-10-13 12:25:34 +00:00
}
return ""
}
func (m *UnfurledStatusChannelLink) GetDescription() string {
if m != nil {
return m.Description
2023-10-13 12:25:34 +00:00
}
return ""
}
func (m *UnfurledStatusChannelLink) GetColor() string {
if m != nil {
return m.Color
2023-10-13 12:25:34 +00:00
}
return ""
}
func (m *UnfurledStatusChannelLink) GetCommunity() *UnfurledStatusCommunityLink {
if m != nil {
return m.Community
2023-10-13 12:25:34 +00:00
}
return nil
}
type UnfurledStatusLink struct {
Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
// Types that are valid to be assigned to Payload:
2023-10-13 12:25:34 +00:00
//
// *UnfurledStatusLink_Contact
// *UnfurledStatusLink_Community
// *UnfurledStatusLink_Channel
Payload isUnfurledStatusLink_Payload `protobuf_oneof:"payload"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
2023-10-13 12:25:34 +00:00
}
func (m *UnfurledStatusLink) Reset() { *m = UnfurledStatusLink{} }
func (m *UnfurledStatusLink) String() string { return proto.CompactTextString(m) }
func (*UnfurledStatusLink) ProtoMessage() {}
func (*UnfurledStatusLink) Descriptor() ([]byte, []int) {
return fileDescriptor_263952f55fd35689, []int{15}
2023-10-13 12:25:34 +00:00
}
func (m *UnfurledStatusLink) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_UnfurledStatusLink.Unmarshal(m, b)
2023-10-13 12:25:34 +00:00
}
func (m *UnfurledStatusLink) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_UnfurledStatusLink.Marshal(b, m, deterministic)
2023-10-13 12:25:34 +00:00
}
func (m *UnfurledStatusLink) XXX_Merge(src proto.Message) {
xxx_messageInfo_UnfurledStatusLink.Merge(m, src)
2023-10-13 12:25:34 +00:00
}
func (m *UnfurledStatusLink) XXX_Size() int {
return xxx_messageInfo_UnfurledStatusLink.Size(m)
2023-10-13 12:25:34 +00:00
}
func (m *UnfurledStatusLink) XXX_DiscardUnknown() {
xxx_messageInfo_UnfurledStatusLink.DiscardUnknown(m)
2023-10-13 12:25:34 +00:00
}
var xxx_messageInfo_UnfurledStatusLink proto.InternalMessageInfo
2023-10-13 12:25:34 +00:00
func (m *UnfurledStatusLink) GetUrl() string {
if m != nil {
return m.Url
}
return ""
2023-10-13 12:25:34 +00:00
}
type isUnfurledStatusLink_Payload interface {
isUnfurledStatusLink_Payload()
}
type UnfurledStatusLink_Contact struct {
Contact *UnfurledStatusContactLink `protobuf:"bytes,2,opt,name=contact,proto3,oneof"`
}
type UnfurledStatusLink_Community struct {
Community *UnfurledStatusCommunityLink `protobuf:"bytes,3,opt,name=community,proto3,oneof"`
}
type UnfurledStatusLink_Channel struct {
Channel *UnfurledStatusChannelLink `protobuf:"bytes,4,opt,name=channel,proto3,oneof"`
}
func (*UnfurledStatusLink_Contact) isUnfurledStatusLink_Payload() {}
func (*UnfurledStatusLink_Community) isUnfurledStatusLink_Payload() {}
func (*UnfurledStatusLink_Channel) isUnfurledStatusLink_Payload() {}
func (m *UnfurledStatusLink) GetPayload() isUnfurledStatusLink_Payload {
if m != nil {
return m.Payload
}
return nil
}
2023-10-13 12:25:34 +00:00
func (m *UnfurledStatusLink) GetContact() *UnfurledStatusContactLink {
if x, ok := m.GetPayload().(*UnfurledStatusLink_Contact); ok {
return x.Contact
}
return nil
}
2023-10-13 12:25:34 +00:00
func (m *UnfurledStatusLink) GetCommunity() *UnfurledStatusCommunityLink {
if x, ok := m.GetPayload().(*UnfurledStatusLink_Community); ok {
return x.Community
}
return nil
}
func (m *UnfurledStatusLink) GetChannel() *UnfurledStatusChannelLink {
if x, ok := m.GetPayload().(*UnfurledStatusLink_Channel); ok {
return x.Channel
}
return nil
}
// XXX_OneofWrappers is for the internal use of the proto package.
func (*UnfurledStatusLink) XXX_OneofWrappers() []interface{} {
return []interface{}{
(*UnfurledStatusLink_Contact)(nil),
(*UnfurledStatusLink_Community)(nil),
(*UnfurledStatusLink_Channel)(nil),
}
}
// Create a wrapper around repeated property for proper unmarshalling
type UnfurledStatusLinks struct {
UnfurledStatusLinks []*UnfurledStatusLink `protobuf:"bytes,1,rep,name=unfurled_status_links,json=unfurledStatusLinks,proto3" json:"unfurled_status_links,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *UnfurledStatusLinks) Reset() { *m = UnfurledStatusLinks{} }
func (m *UnfurledStatusLinks) String() string { return proto.CompactTextString(m) }
func (*UnfurledStatusLinks) ProtoMessage() {}
2023-10-13 12:25:34 +00:00
func (*UnfurledStatusLinks) Descriptor() ([]byte, []int) {
return fileDescriptor_263952f55fd35689, []int{16}
}
func (m *UnfurledStatusLinks) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_UnfurledStatusLinks.Unmarshal(m, b)
}
func (m *UnfurledStatusLinks) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_UnfurledStatusLinks.Marshal(b, m, deterministic)
}
func (m *UnfurledStatusLinks) XXX_Merge(src proto.Message) {
xxx_messageInfo_UnfurledStatusLinks.Merge(m, src)
}
func (m *UnfurledStatusLinks) XXX_Size() int {
return xxx_messageInfo_UnfurledStatusLinks.Size(m)
2023-10-13 12:25:34 +00:00
}
func (m *UnfurledStatusLinks) XXX_DiscardUnknown() {
xxx_messageInfo_UnfurledStatusLinks.DiscardUnknown(m)
}
var xxx_messageInfo_UnfurledStatusLinks proto.InternalMessageInfo
2023-10-13 12:25:34 +00:00
func (m *UnfurledStatusLinks) GetUnfurledStatusLinks() []*UnfurledStatusLink {
if m != nil {
return m.UnfurledStatusLinks
2023-10-13 12:25:34 +00:00
}
return nil
}
type ChatMessage struct {
// Lamport timestamp of the chat message
Clock uint64 `protobuf:"varint,1,opt,name=clock,proto3" json:"clock,omitempty"`
2022-09-28 11:42:17 +00:00
// Unix timestamps in milliseconds, currently not used as we use whisper as
// more reliable, but here so that we don't rely on it
Timestamp uint64 `protobuf:"varint,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
// Text of the message
Text string `protobuf:"bytes,3,opt,name=text,proto3" json:"text,omitempty"`
2020-03-09 07:55:58 +00:00
// Id of the message that we are replying to
ResponseTo string `protobuf:"bytes,4,opt,name=response_to,json=responseTo,proto3" json:"response_to,omitempty"`
// Ens name of the sender
EnsName string `protobuf:"bytes,5,opt,name=ens_name,json=ensName,proto3" json:"ens_name,omitempty"`
// Chat id, this field is symmetric for public-chats and private group chats,
// but asymmetric in case of one-to-ones, as the sender will use the chat-id
// of the received, while the receiver will use the chat-id of the sender.
2022-09-28 11:42:17 +00:00
// Probably should be the concatenation of sender-pk & receiver-pk in
// alphabetical order
ChatId string `protobuf:"bytes,6,opt,name=chat_id,json=chatId,proto3" json:"chat_id,omitempty"`
// The type of message (public/one-to-one/private-group-chat)
MessageType MessageType `protobuf:"varint,7,opt,name=message_type,json=messageType,proto3,enum=protobuf.MessageType" json:"message_type,omitempty"`
// The type of the content of the message
ContentType ChatMessage_ContentType `protobuf:"varint,8,opt,name=content_type,json=contentType,proto3,enum=protobuf.ChatMessage_ContentType" json:"content_type,omitempty"`
// Types that are valid to be assigned to Payload:
//
// *ChatMessage_Sticker
// *ChatMessage_Image
// *ChatMessage_Audio
// *ChatMessage_Community
// *ChatMessage_DiscordMessage
Payload isChatMessage_Payload `protobuf_oneof:"payload"`
// Grant for community chat messages
2022-02-17 15:13:10 +00:00
Grant []byte `protobuf:"bytes,13,opt,name=grant,proto3" json:"grant,omitempty"`
// Message author's display name, introduced in version 1
DisplayName string `protobuf:"bytes,14,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"`
ContactRequestPropagatedState *ContactRequestPropagatedState `protobuf:"bytes,15,opt,name=contact_request_propagated_state,json=contactRequestPropagatedState,proto3" json:"contact_request_propagated_state,omitempty"`
URL unfurling (initial implementation) (#3471) This is the initial implementation for the new URL unfurling requirements. The most important one is that only the message sender will pay the privacy cost for unfurling and extracting metadata from websites. Once the message is sent, the unfurled data will be stored at the protocol level and receivers will just profit and happily decode the metadata to render it. Further development of this URL unfurling capability will be mostly guided by issues created on clients. For the moment in status-mobile: https://github.com/status-im/status-mobile/labels/url-preview - https://github.com/status-im/status-mobile/issues/15918 - https://github.com/status-im/status-mobile/issues/15917 - https://github.com/status-im/status-mobile/issues/15910 - https://github.com/status-im/status-mobile/issues/15909 - https://github.com/status-im/status-mobile/issues/15908 - https://github.com/status-im/status-mobile/issues/15906 - https://github.com/status-im/status-mobile/issues/15905 ### Terminology In the code, I've tried to stick to the word "unfurl URL" to really mean the process of extracting metadata from a website, sort of lower level. I use "link preview" to mean a higher level structure which is enriched by unfurled data. "link preview" is also how designers refer to it. ### User flows 1. Carol needs to see link previews while typing in the chat input field. Notice from the diagram nothing is persisted and that status-go endpoints are essentially stateless. ``` #+begin_src plantuml :results verbatim Client->>Server: Call wakuext_getTextURLs Server-->>Client: Normalized URLs Client->>Client: Render cached unfurled URLs Client->>Server: Unfurl non-cached URLs.\nCall wakuext_unfurlURLs Server->>Website: Fetch metadata Website-->>Server: Metadata (thumbnail URL, title, etc) Server->>Website: Fetch thumbnail Server->>Website: Fetch favicon Website-->>Server: Favicon bytes Website-->>Server: Thumbnail bytes Server->>Server: Decode & process images Server-->>Client: Unfurled data (thumbnail data URI, etc) #+end_src ``` ``` ,------. ,------. ,-------. |Client| |Server| |Website| `--+---' `--+---' `---+---' | Call wakuext_getTextURLs | | | ---------------------------------------> | | | | | Normalized URLs | | | <- - - - - - - - - - - - - - - - - - - - | | | | |----. | | | | Render cached unfurled URLs | | |<---' | | | | | | Unfurl non-cached URLs. | | | Call wakuext_unfurlURLs | | | ---------------------------------------> | | | | | | Fetch metadata | | | ------------------------------------> | | | | | Metadata (thumbnail URL, title, etc)| | | <- - - - - - - - - - - - - - - - - - | | | | | Fetch thumbnail | | | ------------------------------------> | | | | | Fetch favicon | | | ------------------------------------> | | | | | Favicon bytes | | | <- - - - - - - - - - - - - - - - - - | | | | | Thumbnail bytes | | | <- - - - - - - - - - - - - - - - - - | | | | |----. | | | | Decode & process images | | |<---' | | | | | Unfurled data (thumbnail data URI, etc)| | | <- - - - - - - - - - - - - - - - - - - - | ,--+---. ,--+---. ,---+---. |Client| |Server| |Website| `------' `------' `-------' ``` 2. Carol sends the text message with link previews in the RPC request wakuext_sendChatMessages. status-go assumes the link previews are good because it can't and shouldn't attempt to re-unfurl them. ``` #+begin_src plantuml :results verbatim Client->>Server: Call wakuext_sendChatMessages Server->>Server: Transform link previews to\nbe proto-marshalled Server->DB: Write link previews serialized as JSON Server-->>Client: Updated message response #+end_src ``` ``` ,------. ,------. ,--. |Client| |Server| |DB| `--+---' `--+---' `+-' | Call wakuext_sendChatMessages| | | -----------------------------> | | | | | |----. | | | | Transform link previews to | | |<---' be proto-marshalled | | | | | | | | | Write link previews serialized as JSON| | | --------------------------------------> | | | | Updated message response | | | <- - - - - - - - - - - - - - - | ,--+---. ,--+---. ,+-. |Client| |Server| |DB| `------' `------' `--' ``` 3. The message was sent over waku and persisted locally in Carol's device. She should now see the link previews in the chat history. There can be many link previews shared by other chat members, therefore it is important to serve the assets via the media server to avoid overloading the ReactNative bridge with lots of big JSON payloads containing base64 encoded data URIs (maybe this concern is meaningless for desktop). When a client is rendering messages with link previews, they will have the field linkPreviews, and the thumbnail URL will point to the local media server. ``` #+begin_src plantuml :results verbatim Client->>Server: GET /link-preview/thumbnail (media server) Server->>DB: Read from user_messages.unfurled_links Server->Server: Unmarshal JSON Server-->>Client: HTTP Content-Type: image/jpeg/etc #+end_src ``` ``` ,------. ,------. ,--. |Client| |Server| |DB| `--+---' `--+---' `+-' | GET /link-preview/thumbnail (media server)| | | ------------------------------------------> | | | | | | Read from user_messages.unfurled_links| | | --------------------------------------> | | | | |----. | | | | Unmarshal JSON | | |<---' | | | | | HTTP Content-Type: image/jpeg/etc | | | <- - - - - - - - - - - - - - - - - - - - - | ,--+---. ,--+---. ,+-. |Client| |Server| |DB| `------' `------' `--' ``` ### Some limitations of the current implementation The following points will become separate issues in status-go that I'll work on over the next couple weeks. In no order of importance: - Improve how multiple links are fetched; retries on failure and testing how unfurling behaves around the timeout limits (deterministically, not by making real HTTP calls as I did). https://github.com/status-im/status-go/issues/3498 - Unfurl favicons and store them in the protobuf too. - For this PR, I added unfurling support only for websites with OpenGraph https://ogp.me/ meta tags. Other unfurlers will be implemented on demand. The next one will probably be for oEmbed https://oembed.com/, the protocol supported by YouTube, for example. - Resize and/or compress thumbnails (and favicons). Often times, thumbnails are huge for the purposes of link previews. There is already support for compressing JPEGs in status-go, but I prefer to work with compression in a separate PR because I'd like to also solve the problem for PNGs (probably convert them to JPEGs, plus compress them). This would be a safe choice for thumbnails, favicons not so much because transparency is desirable. - Editing messages is not yet supported. - I haven't coded any artificial limit on the number of previews or on the size of the thumbnail payload. This will be done in a separate issue. I have heard the ideal solution may be to split messages into smaller chunks of ~125 KiB because of libp2p, but that might be too complicated at this stage of the product (?). - Link preview deletion. - For the moment, OpenGraph metadata is extracted by requesting data for the English language (and fallback to whatever is available). In the future, we'll want to unfurl by respecting the user's local device language. Some websites, like GoDaddy, are already localized based on the device's IP, but many aren't. - The website's description text should be limited by a certain number of characters, especially because it's outside our control. Exactly how much has not been decided yet, so it'll be done separately. - URL normalization can be tricky, so I implemented only the basics to help with caching. For example, the url https://status.im and HTTPS://status.im are considered identical. Also, a URL is considered valid for unfurling if its TLD exists according to publicsuffix.EffectiveTLDPlusOne. This was essential, otherwise the default Go url.Parse approach would consider many invalid URLs valid, and thus the server would waste resources trying to unfurl the unfurleable. ### Other requirements - If the message is edited, the link previews should reflect the edited text, not the original one. This has been aligned with the design team as well. - If the website's thumbnail or the favicon can't be fetched, just ignore them. The only mandatory piece of metadata is the website's title and URL. - Link previews in clients should be generated in near real-time, that is, as the user types, previews are updated. In mobile this performs very well, and it's what other clients like WhatsApp, Telegram, and Facebook do. ### Decisions - While the user typing in the input field, the client is constantly (debounced) asking status-go to parse the text and extract normalized URLs and then the client checks if they're already in its in-memory cache. If they are, no RPC call is made. I chose this approach to achieve the best possible performance in mobile and avoid the whole RPC overhead, since the chat experience is already not smooth enough. The mobile client uses URLs as cache keys in a hashmap, i.e. if the key is present, it means the preview is readily available (naive, but good enough for now). This decision also gave me more flexibility to find the best UX at this stage of the feature. - Due to the requirement that users should be able to see independent loading indicators for each link preview, when status-go can't unfurl a URL, it doesn't return it in the response. - As an initial implementation, I added the BLOB column unfurled_links to the user_messages table. The preview data is then serialized as JSON before being stored in this column. I felt that creating a separate table and the related code for this initial PR would be inconvenient. Is that reasonable to you? Once things stabilize I can create a proper table if we want to avoid this kind of solution with serialized columns.
2023-05-18 18:43:06 +00:00
UnfurledLinks []*UnfurledLink `protobuf:"bytes,16,rep,name=unfurled_links,json=unfurledLinks,proto3" json:"unfurled_links,omitempty"`
Shard *Shard `protobuf:"bytes,17,opt,name=shard,proto3" json:"shard,omitempty"`
2023-10-13 12:25:34 +00:00
UnfurledStatusLinks *UnfurledStatusLinks `protobuf:"bytes,18,opt,name=unfurled_status_links,json=unfurledStatusLinks,proto3" json:"unfurled_status_links,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ChatMessage) Reset() { *m = ChatMessage{} }
func (m *ChatMessage) String() string { return proto.CompactTextString(m) }
func (*ChatMessage) ProtoMessage() {}
func (*ChatMessage) Descriptor() ([]byte, []int) {
return fileDescriptor_263952f55fd35689, []int{17}
}
func (m *ChatMessage) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ChatMessage.Unmarshal(m, b)
}
func (m *ChatMessage) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ChatMessage.Marshal(b, m, deterministic)
}
func (m *ChatMessage) XXX_Merge(src proto.Message) {
xxx_messageInfo_ChatMessage.Merge(m, src)
}
func (m *ChatMessage) XXX_Size() int {
return xxx_messageInfo_ChatMessage.Size(m)
}
func (m *ChatMessage) XXX_DiscardUnknown() {
xxx_messageInfo_ChatMessage.DiscardUnknown(m)
}
var xxx_messageInfo_ChatMessage proto.InternalMessageInfo
func (m *ChatMessage) GetClock() uint64 {
if m != nil {
return m.Clock
}
return 0
}
func (m *ChatMessage) GetTimestamp() uint64 {
if m != nil {
return m.Timestamp
}
return 0
}
func (m *ChatMessage) GetText() string {
if m != nil {
return m.Text
}
return ""
}
func (m *ChatMessage) GetResponseTo() string {
if m != nil {
return m.ResponseTo
}
return ""
}
func (m *ChatMessage) GetEnsName() string {
if m != nil {
return m.EnsName
}
return ""
}
func (m *ChatMessage) GetChatId() string {
if m != nil {
return m.ChatId
}
return ""
}
func (m *ChatMessage) GetMessageType() MessageType {
if m != nil {
return m.MessageType
}
return MessageType_UNKNOWN_MESSAGE_TYPE
}
func (m *ChatMessage) GetContentType() ChatMessage_ContentType {
if m != nil {
return m.ContentType
}
return ChatMessage_UNKNOWN_CONTENT_TYPE
}
type isChatMessage_Payload interface {
isChatMessage_Payload()
}
type ChatMessage_Sticker struct {
Sticker *StickerMessage `protobuf:"bytes,9,opt,name=sticker,proto3,oneof"`
}
type ChatMessage_Image struct {
Image *ImageMessage `protobuf:"bytes,10,opt,name=image,proto3,oneof"`
}
type ChatMessage_Audio struct {
Audio *AudioMessage `protobuf:"bytes,11,opt,name=audio,proto3,oneof"`
}
type ChatMessage_Community struct {
Community []byte `protobuf:"bytes,12,opt,name=community,proto3,oneof"`
}
type ChatMessage_DiscordMessage struct {
DiscordMessage *DiscordMessage `protobuf:"bytes,99,opt,name=discord_message,json=discordMessage,proto3,oneof"`
}
func (*ChatMessage_Sticker) isChatMessage_Payload() {}
func (*ChatMessage_Image) isChatMessage_Payload() {}
func (*ChatMessage_Audio) isChatMessage_Payload() {}
func (*ChatMessage_Community) isChatMessage_Payload() {}
func (*ChatMessage_DiscordMessage) isChatMessage_Payload() {}
func (m *ChatMessage) GetPayload() isChatMessage_Payload {
if m != nil {
return m.Payload
}
return nil
}
func (m *ChatMessage) GetSticker() *StickerMessage {
if x, ok := m.GetPayload().(*ChatMessage_Sticker); ok {
return x.Sticker
}
return nil
}
func (m *ChatMessage) GetImage() *ImageMessage {
if x, ok := m.GetPayload().(*ChatMessage_Image); ok {
return x.Image
}
return nil
}
func (m *ChatMessage) GetAudio() *AudioMessage {
if x, ok := m.GetPayload().(*ChatMessage_Audio); ok {
return x.Audio
}
return nil
}
func (m *ChatMessage) GetCommunity() []byte {
if x, ok := m.GetPayload().(*ChatMessage_Community); ok {
return x.Community
}
return nil
}
func (m *ChatMessage) GetDiscordMessage() *DiscordMessage {
if x, ok := m.GetPayload().(*ChatMessage_DiscordMessage); ok {
return x.DiscordMessage
}
return nil
}
func (m *ChatMessage) GetGrant() []byte {
if m != nil {
return m.Grant
}
return nil
}
func (m *ChatMessage) GetDisplayName() string {
if m != nil {
return m.DisplayName
2022-02-17 15:13:10 +00:00
}
return ""
}
func (m *ChatMessage) GetContactRequestPropagatedState() *ContactRequestPropagatedState {
if m != nil {
return m.ContactRequestPropagatedState
}
return nil
}
func (m *ChatMessage) GetUnfurledLinks() []*UnfurledLink {
if m != nil {
return m.UnfurledLinks
URL unfurling (initial implementation) (#3471) This is the initial implementation for the new URL unfurling requirements. The most important one is that only the message sender will pay the privacy cost for unfurling and extracting metadata from websites. Once the message is sent, the unfurled data will be stored at the protocol level and receivers will just profit and happily decode the metadata to render it. Further development of this URL unfurling capability will be mostly guided by issues created on clients. For the moment in status-mobile: https://github.com/status-im/status-mobile/labels/url-preview - https://github.com/status-im/status-mobile/issues/15918 - https://github.com/status-im/status-mobile/issues/15917 - https://github.com/status-im/status-mobile/issues/15910 - https://github.com/status-im/status-mobile/issues/15909 - https://github.com/status-im/status-mobile/issues/15908 - https://github.com/status-im/status-mobile/issues/15906 - https://github.com/status-im/status-mobile/issues/15905 ### Terminology In the code, I've tried to stick to the word "unfurl URL" to really mean the process of extracting metadata from a website, sort of lower level. I use "link preview" to mean a higher level structure which is enriched by unfurled data. "link preview" is also how designers refer to it. ### User flows 1. Carol needs to see link previews while typing in the chat input field. Notice from the diagram nothing is persisted and that status-go endpoints are essentially stateless. ``` #+begin_src plantuml :results verbatim Client->>Server: Call wakuext_getTextURLs Server-->>Client: Normalized URLs Client->>Client: Render cached unfurled URLs Client->>Server: Unfurl non-cached URLs.\nCall wakuext_unfurlURLs Server->>Website: Fetch metadata Website-->>Server: Metadata (thumbnail URL, title, etc) Server->>Website: Fetch thumbnail Server->>Website: Fetch favicon Website-->>Server: Favicon bytes Website-->>Server: Thumbnail bytes Server->>Server: Decode & process images Server-->>Client: Unfurled data (thumbnail data URI, etc) #+end_src ``` ``` ,------. ,------. ,-------. |Client| |Server| |Website| `--+---' `--+---' `---+---' | Call wakuext_getTextURLs | | | ---------------------------------------> | | | | | Normalized URLs | | | <- - - - - - - - - - - - - - - - - - - - | | | | |----. | | | | Render cached unfurled URLs | | |<---' | | | | | | Unfurl non-cached URLs. | | | Call wakuext_unfurlURLs | | | ---------------------------------------> | | | | | | Fetch metadata | | | ------------------------------------> | | | | | Metadata (thumbnail URL, title, etc)| | | <- - - - - - - - - - - - - - - - - - | | | | | Fetch thumbnail | | | ------------------------------------> | | | | | Fetch favicon | | | ------------------------------------> | | | | | Favicon bytes | | | <- - - - - - - - - - - - - - - - - - | | | | | Thumbnail bytes | | | <- - - - - - - - - - - - - - - - - - | | | | |----. | | | | Decode & process images | | |<---' | | | | | Unfurled data (thumbnail data URI, etc)| | | <- - - - - - - - - - - - - - - - - - - - | ,--+---. ,--+---. ,---+---. |Client| |Server| |Website| `------' `------' `-------' ``` 2. Carol sends the text message with link previews in the RPC request wakuext_sendChatMessages. status-go assumes the link previews are good because it can't and shouldn't attempt to re-unfurl them. ``` #+begin_src plantuml :results verbatim Client->>Server: Call wakuext_sendChatMessages Server->>Server: Transform link previews to\nbe proto-marshalled Server->DB: Write link previews serialized as JSON Server-->>Client: Updated message response #+end_src ``` ``` ,------. ,------. ,--. |Client| |Server| |DB| `--+---' `--+---' `+-' | Call wakuext_sendChatMessages| | | -----------------------------> | | | | | |----. | | | | Transform link previews to | | |<---' be proto-marshalled | | | | | | | | | Write link previews serialized as JSON| | | --------------------------------------> | | | | Updated message response | | | <- - - - - - - - - - - - - - - | ,--+---. ,--+---. ,+-. |Client| |Server| |DB| `------' `------' `--' ``` 3. The message was sent over waku and persisted locally in Carol's device. She should now see the link previews in the chat history. There can be many link previews shared by other chat members, therefore it is important to serve the assets via the media server to avoid overloading the ReactNative bridge with lots of big JSON payloads containing base64 encoded data URIs (maybe this concern is meaningless for desktop). When a client is rendering messages with link previews, they will have the field linkPreviews, and the thumbnail URL will point to the local media server. ``` #+begin_src plantuml :results verbatim Client->>Server: GET /link-preview/thumbnail (media server) Server->>DB: Read from user_messages.unfurled_links Server->Server: Unmarshal JSON Server-->>Client: HTTP Content-Type: image/jpeg/etc #+end_src ``` ``` ,------. ,------. ,--. |Client| |Server| |DB| `--+---' `--+---' `+-' | GET /link-preview/thumbnail (media server)| | | ------------------------------------------> | | | | | | Read from user_messages.unfurled_links| | | --------------------------------------> | | | | |----. | | | | Unmarshal JSON | | |<---' | | | | | HTTP Content-Type: image/jpeg/etc | | | <- - - - - - - - - - - - - - - - - - - - - | ,--+---. ,--+---. ,+-. |Client| |Server| |DB| `------' `------' `--' ``` ### Some limitations of the current implementation The following points will become separate issues in status-go that I'll work on over the next couple weeks. In no order of importance: - Improve how multiple links are fetched; retries on failure and testing how unfurling behaves around the timeout limits (deterministically, not by making real HTTP calls as I did). https://github.com/status-im/status-go/issues/3498 - Unfurl favicons and store them in the protobuf too. - For this PR, I added unfurling support only for websites with OpenGraph https://ogp.me/ meta tags. Other unfurlers will be implemented on demand. The next one will probably be for oEmbed https://oembed.com/, the protocol supported by YouTube, for example. - Resize and/or compress thumbnails (and favicons). Often times, thumbnails are huge for the purposes of link previews. There is already support for compressing JPEGs in status-go, but I prefer to work with compression in a separate PR because I'd like to also solve the problem for PNGs (probably convert them to JPEGs, plus compress them). This would be a safe choice for thumbnails, favicons not so much because transparency is desirable. - Editing messages is not yet supported. - I haven't coded any artificial limit on the number of previews or on the size of the thumbnail payload. This will be done in a separate issue. I have heard the ideal solution may be to split messages into smaller chunks of ~125 KiB because of libp2p, but that might be too complicated at this stage of the product (?). - Link preview deletion. - For the moment, OpenGraph metadata is extracted by requesting data for the English language (and fallback to whatever is available). In the future, we'll want to unfurl by respecting the user's local device language. Some websites, like GoDaddy, are already localized based on the device's IP, but many aren't. - The website's description text should be limited by a certain number of characters, especially because it's outside our control. Exactly how much has not been decided yet, so it'll be done separately. - URL normalization can be tricky, so I implemented only the basics to help with caching. For example, the url https://status.im and HTTPS://status.im are considered identical. Also, a URL is considered valid for unfurling if its TLD exists according to publicsuffix.EffectiveTLDPlusOne. This was essential, otherwise the default Go url.Parse approach would consider many invalid URLs valid, and thus the server would waste resources trying to unfurl the unfurleable. ### Other requirements - If the message is edited, the link previews should reflect the edited text, not the original one. This has been aligned with the design team as well. - If the website's thumbnail or the favicon can't be fetched, just ignore them. The only mandatory piece of metadata is the website's title and URL. - Link previews in clients should be generated in near real-time, that is, as the user types, previews are updated. In mobile this performs very well, and it's what other clients like WhatsApp, Telegram, and Facebook do. ### Decisions - While the user typing in the input field, the client is constantly (debounced) asking status-go to parse the text and extract normalized URLs and then the client checks if they're already in its in-memory cache. If they are, no RPC call is made. I chose this approach to achieve the best possible performance in mobile and avoid the whole RPC overhead, since the chat experience is already not smooth enough. The mobile client uses URLs as cache keys in a hashmap, i.e. if the key is present, it means the preview is readily available (naive, but good enough for now). This decision also gave me more flexibility to find the best UX at this stage of the feature. - Due to the requirement that users should be able to see independent loading indicators for each link preview, when status-go can't unfurl a URL, it doesn't return it in the response. - As an initial implementation, I added the BLOB column unfurled_links to the user_messages table. The preview data is then serialized as JSON before being stored in this column. I felt that creating a separate table and the related code for this initial PR would be inconvenient. Is that reasonable to you? Once things stabilize I can create a proper table if we want to avoid this kind of solution with serialized columns.
2023-05-18 18:43:06 +00:00
}
return nil
}
func (m *ChatMessage) GetShard() *Shard {
if m != nil {
return m.Shard
}
return nil
}
func (m *ChatMessage) GetUnfurledStatusLinks() *UnfurledStatusLinks {
if m != nil {
return m.UnfurledStatusLinks
2023-10-13 12:25:34 +00:00
}
return nil
}
// XXX_OneofWrappers is for the internal use of the proto package.
func (*ChatMessage) XXX_OneofWrappers() []interface{} {
return []interface{}{
(*ChatMessage_Sticker)(nil),
(*ChatMessage_Image)(nil),
(*ChatMessage_Audio)(nil),
(*ChatMessage_Community)(nil),
(*ChatMessage_DiscordMessage)(nil),
}
}
func init() {
proto.RegisterEnum("protobuf.AudioMessage_AudioType", AudioMessage_AudioType_name, AudioMessage_AudioType_value)
proto.RegisterEnum("protobuf.UnfurledLink_LinkType", UnfurledLink_LinkType_name, UnfurledLink_LinkType_value)
proto.RegisterEnum("protobuf.ChatMessage_ContentType", ChatMessage_ContentType_name, ChatMessage_ContentType_value)
proto.RegisterType((*StickerMessage)(nil), "protobuf.StickerMessage")
proto.RegisterType((*ImageMessage)(nil), "protobuf.ImageMessage")
proto.RegisterType((*AudioMessage)(nil), "protobuf.AudioMessage")
proto.RegisterType((*EditMessage)(nil), "protobuf.EditMessage")
proto.RegisterType((*DeleteMessage)(nil), "protobuf.DeleteMessage")
proto.RegisterType((*SyncDeleteForMeMessage)(nil), "protobuf.SyncDeleteForMeMessage")
proto.RegisterType((*DiscordMessage)(nil), "protobuf.DiscordMessage")
proto.RegisterType((*DiscordMessageAuthor)(nil), "protobuf.DiscordMessageAuthor")
proto.RegisterType((*DiscordMessageReference)(nil), "protobuf.DiscordMessageReference")
proto.RegisterType((*DiscordMessageAttachment)(nil), "protobuf.DiscordMessageAttachment")
proto.RegisterType((*UnfurledLinkThumbnail)(nil), "protobuf.UnfurledLinkThumbnail")
proto.RegisterType((*UnfurledLink)(nil), "protobuf.UnfurledLink")
proto.RegisterType((*UnfurledStatusContactLink)(nil), "protobuf.UnfurledStatusContactLink")
proto.RegisterType((*UnfurledStatusCommunityLink)(nil), "protobuf.UnfurledStatusCommunityLink")
proto.RegisterType((*UnfurledStatusChannelLink)(nil), "protobuf.UnfurledStatusChannelLink")
proto.RegisterType((*UnfurledStatusLink)(nil), "protobuf.UnfurledStatusLink")
proto.RegisterType((*UnfurledStatusLinks)(nil), "protobuf.UnfurledStatusLinks")
proto.RegisterType((*ChatMessage)(nil), "protobuf.ChatMessage")
}
func init() {
proto.RegisterFile("chat_message.proto", fileDescriptor_263952f55fd35689)
}
var fileDescriptor_263952f55fd35689 = []byte{
// 1841 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x58, 0xdd, 0x92, 0xdb, 0x58,
0x11, 0x1e, 0xff, 0x8f, 0x5a, 0xb6, 0x47, 0x39, 0x33, 0x99, 0x38, 0x21, 0xb3, 0x71, 0x94, 0x6c,
0x65, 0x28, 0xa8, 0xa1, 0x6a, 0xb2, 0xc0, 0x56, 0x51, 0xd4, 0x96, 0x62, 0x6b, 0x13, 0x91, 0xd8,
0xe3, 0x95, 0xe5, 0x2c, 0xe1, 0x46, 0xa5, 0x91, 0x4e, 0xc6, 0x62, 0xf4, 0x63, 0xf4, 0x03, 0x98,
0x17, 0xe1, 0x2d, 0xe0, 0x86, 0xe2, 0x8a, 0x07, 0xe0, 0x82, 0x3b, 0x8a, 0x57, 0xe0, 0x09, 0xb8,
0xe2, 0x8a, 0x3a, 0x3f, 0xfa, 0xf3, 0xd8, 0x93, 0x4d, 0x8a, 0x2b, 0xab, 0xfb, 0x74, 0x9f, 0xd3,
0xfd, 0x75, 0x9f, 0x3e, 0xdd, 0x06, 0x64, 0x2f, 0xad, 0xc4, 0xf4, 0x71, 0x1c, 0x5b, 0x57, 0xf8,
0x6c, 0x15, 0x85, 0x49, 0x88, 0xf6, 0xe9, 0xcf, 0x65, 0xfa, 0xfe, 0x81, 0x88, 0x83, 0xd4, 0x8f,
0x19, 0xfb, 0x41, 0xcf, 0x0e, 0x83, 0xc4, 0xb2, 0x13, 0x4e, 0x8a, 0xf1, 0xd2, 0x8a, 0x1c, 0x46,
0xc8, 0x5f, 0x42, 0x7f, 0x9e, 0xb8, 0xf6, 0x35, 0x8e, 0x26, 0x6c, 0x2b, 0x84, 0xa0, 0xb9, 0xb4,
0xe2, 0xe5, 0xa0, 0x36, 0xac, 0x9d, 0x0a, 0x3a, 0xfd, 0x26, 0xbc, 0x95, 0x65, 0x5f, 0x0f, 0xea,
0xc3, 0xda, 0x69, 0x4b, 0xa7, 0xdf, 0xf2, 0xdf, 0x6b, 0xd0, 0xd5, 0x7c, 0xeb, 0x0a, 0x67, 0x8a,
0x03, 0xe8, 0xac, 0xac, 0xb5, 0x17, 0x5a, 0x0e, 0xd5, 0xed, 0xea, 0x19, 0x89, 0x9e, 0x41, 0x33,
0x59, 0xaf, 0x30, 0x55, 0xef, 0x9f, 0x1f, 0x9e, 0x65, 0x66, 0x9e, 0x51, 0x7d, 0x63, 0xbd, 0xc2,
0x3a, 0x15, 0x40, 0xf7, 0x61, 0xdf, 0xf2, 0x2e, 0x53, 0xdf, 0x74, 0x9d, 0x41, 0x83, 0x9e, 0xdf,
0xa1, 0xb4, 0xe6, 0xa0, 0x23, 0x68, 0xfd, 0xce, 0x75, 0x92, 0xe5, 0xa0, 0x39, 0xac, 0x9d, 0xf6,
0x74, 0x46, 0xa0, 0x63, 0x68, 0x2f, 0xb1, 0x7b, 0xb5, 0x4c, 0x06, 0x2d, 0xca, 0xe6, 0x14, 0xfa,
0x21, 0x20, 0xbe, 0x11, 0x39, 0x21, 0x36, 0xed, 0x30, 0x0d, 0x92, 0x41, 0x9b, 0xca, 0x48, 0x6c,
0x4b, 0xba, 0x30, 0x22, 0x7c, 0xf9, 0xaf, 0x35, 0xe8, 0x2a, 0xa9, 0xe3, 0x86, 0x1f, 0x76, 0xe5,
0x8b, 0x8a, 0x2b, 0xc3, 0xc2, 0x95, 0xb2, 0x3e, 0x23, 0x4a, 0x7e, 0x3d, 0x02, 0xd1, 0x49, 0x23,
0x2b, 0x71, 0xc3, 0xc0, 0xf4, 0x63, 0xea, 0x5a, 0x53, 0x87, 0x8c, 0x35, 0x89, 0xe5, 0x1f, 0x83,
0x90, 0xeb, 0xa0, 0x63, 0x40, 0x8b, 0xe9, 0xeb, 0xe9, 0xc5, 0xb7, 0x53, 0x53, 0x59, 0x8c, 0xb5,
0x0b, 0xd3, 0x78, 0x37, 0x53, 0xa5, 0x3d, 0xd4, 0x81, 0x86, 0xa2, 0x8c, 0xa4, 0x1a, 0xfd, 0x98,
0xe8, 0x52, 0x5d, 0xfe, 0x63, 0x03, 0x44, 0xd5, 0x71, 0x93, 0xcc, 0xee, 0x23, 0x68, 0xd9, 0x5e,
0x68, 0x5f, 0x53, 0xab, 0x9b, 0x3a, 0x23, 0x48, 0xf4, 0x12, 0xfc, 0xfb, 0x84, 0xda, 0x2c, 0xe8,
0xf4, 0x1b, 0xdd, 0x83, 0x0e, 0x4d, 0xa0, 0x1c, 0xe8, 0x36, 0x21, 0x35, 0x07, 0x9d, 0x00, 0xf0,
0xa4, 0x22, 0x6b, 0x4d, 0xba, 0x26, 0x70, 0x0e, 0x0b, 0xc3, 0x55, 0x64, 0x05, 0x0c, 0xef, 0xae,
0xce, 0x08, 0xf4, 0x25, 0x74, 0x33, 0x25, 0x8a, 0x4e, 0x9b, 0xa2, 0x73, 0xb7, 0x40, 0x87, 0x1b,
0x48, 0x21, 0x11, 0xfd, 0x82, 0x40, 0x63, 0xe8, 0x92, 0xec, 0xc4, 0x41, 0xc2, 0x34, 0x3b, 0x54,
0xf3, 0x71, 0xa1, 0x39, 0x5a, 0x5a, 0x99, 0x7b, 0x67, 0x23, 0x26, 0xc9, 0x76, 0xb1, 0x0b, 0x02,
0xfd, 0x1c, 0xfa, 0x69, 0xf0, 0x3e, 0x8d, 0x3c, 0xec, 0x98, 0x9e, 0x1b, 0x5c, 0xc7, 0x83, 0xfd,
0x61, 0xe3, 0x54, 0x3c, 0x3f, 0x2e, 0xf6, 0x59, 0xf0, 0xf5, 0x37, 0x6e, 0x70, 0xad, 0xf7, 0xd2,
0x12, 0x15, 0xa3, 0x6f, 0xe0, 0x6e, 0xae, 0x1e, 0x27, 0x56, 0x92, 0xc6, 0x7c, 0x17, 0x61, 0x58,
0x3b, 0x15, 0xcf, 0x4f, 0x6e, 0xee, 0x32, 0xa7, 0x52, 0x54, 0x5b, 0x3f, 0x4c, 0x6f, 0x32, 0xe5,
0x7f, 0xd4, 0xa0, 0x37, 0xc6, 0x1e, 0x4e, 0xf0, 0xed, 0xb1, 0x29, 0xc5, 0xa1, 0x7e, 0x4b, 0x1c,
0x1a, 0x3b, 0xe3, 0xd0, 0xbc, 0x2d, 0x0e, 0xad, 0xef, 0x1c, 0x87, 0x13, 0x00, 0x87, 0x9a, 0xeb,
0x98, 0x97, 0x6b, 0x1a, 0x3f, 0x41, 0x17, 0x38, 0xe7, 0xc5, 0x5a, 0x9e, 0xc0, 0xf1, 0x7c, 0x1d,
0xd8, 0xcc, 0xa3, 0xaf, 0xc3, 0x68, 0xf2, 0x01, 0xb7, 0xaa, 0xd6, 0xd7, 0x37, 0xac, 0x97, 0xff,
0x55, 0x87, 0xfe, 0xd8, 0x8d, 0xed, 0x30, 0x72, 0xb2, 0x7d, 0xfa, 0x50, 0x77, 0x1d, 0x5e, 0x74,
0xea, 0xae, 0x43, 0x93, 0x36, 0xbb, 0x68, 0x02, 0xbf, 0x46, 0x0f, 0x41, 0x48, 0x5c, 0x1f, 0xc7,
0x89, 0xe5, 0xaf, 0x32, 0x48, 0x72, 0x06, 0x3a, 0x85, 0x83, 0x9c, 0x20, 0x97, 0x02, 0x67, 0xe9,
0xbb, 0xc9, 0x26, 0xd7, 0x9b, 0x67, 0x0f, 0x45, 0x48, 0xd0, 0x33, 0x12, 0xfd, 0x04, 0xda, 0x56,
0x9a, 0x2c, 0xc3, 0x88, 0x42, 0x20, 0x9e, 0x7f, 0x56, 0x40, 0x57, 0xb5, 0x57, 0xa1, 0x52, 0x3a,
0x97, 0x46, 0x5f, 0x81, 0x10, 0xe1, 0xf7, 0x38, 0xc2, 0x81, 0xcd, 0x72, 0x58, 0x2c, 0xe7, 0x70,
0x55, 0x55, 0xcf, 0x04, 0xf5, 0x42, 0x07, 0x8d, 0x41, 0xb4, 0x92, 0xc4, 0xb2, 0x97, 0x3e, 0x0e,
0x92, 0x2c, 0x7d, 0xe5, 0x9d, 0xa7, 0xe7, 0xa2, 0x7a, 0x59, 0x4d, 0xfe, 0x77, 0x0d, 0x8e, 0xb6,
0xd9, 0xb9, 0x0d, 0xdd, 0xc0, 0xf2, 0x73, 0x74, 0xc9, 0x37, 0x7a, 0x0a, 0x3d, 0xc7, 0x8d, 0xed,
0xc8, 0xf5, 0xdd, 0xc0, 0x4a, 0xc2, 0x88, 0x23, 0x5c, 0x65, 0xa2, 0x07, 0xb0, 0x1f, 0xb8, 0xf6,
0x35, 0xd5, 0x66, 0xf0, 0xe6, 0x34, 0x89, 0x8f, 0xf5, 0x5b, 0x2b, 0xb1, 0xa2, 0x45, 0xe4, 0x71,
0x64, 0x0b, 0x06, 0x3a, 0x03, 0xc4, 0x08, 0x5a, 0x7a, 0x67, 0xbc, 0xbe, 0xb6, 0x69, 0xfe, 0x6e,
0x59, 0x21, 0x27, 0x79, 0xa1, 0x6d, 0x79, 0x64, 0xb3, 0x0e, 0x3b, 0x29, 0xa3, 0xe5, 0x10, 0xee,
0xed, 0x00, 0x95, 0x18, 0x91, 0x27, 0x1a, 0xf7, 0xb8, 0x74, 0x6f, 0x1e, 0x82, 0x60, 0x2f, 0xad,
0x20, 0xc0, 0x9e, 0x96, 0xe7, 0x65, 0xce, 0x20, 0x89, 0x71, 0x95, 0xba, 0x9e, 0xa3, 0xe5, 0xcf,
0x0f, 0x27, 0xe5, 0xff, 0xd4, 0x60, 0xb0, 0x2b, 0x06, 0x37, 0xd0, 0xad, 0x98, 0xb0, 0x99, 0xfc,
0x48, 0x82, 0x46, 0x1a, 0x79, 0xfc, 0x00, 0xf2, 0x49, 0x3c, 0x7d, 0xef, 0x7a, 0x78, 0x5a, 0xc2,
0x34, 0xa3, 0x49, 0x54, 0xc8, 0xf7, 0xdc, 0xfd, 0x03, 0x7e, 0xb1, 0x4e, 0x70, 0x4c, 0x71, 0x6d,
0xea, 0x55, 0x26, 0x1a, 0x42, 0xb9, 0x1e, 0xf2, 0xfb, 0x5b, 0x29, 0x91, 0xa5, 0x27, 0xad, 0x53,
0x7d, 0xd2, 0xca, 0x38, 0xef, 0x6f, 0xe0, 0x6c, 0xc2, 0xdd, 0x72, 0xe1, 0x34, 0x96, 0xa9, 0x7f,
0x19, 0x58, 0xae, 0x77, 0xcb, 0x0b, 0x99, 0x3f, 0xd4, 0xf5, 0xed, 0x0f, 0x75, 0xa3, 0xfc, 0x50,
0xcb, 0x7f, 0xae, 0x43, 0xb7, 0x7c, 0x42, 0x86, 0x4e, 0xad, 0x40, 0xe7, 0x08, 0x5a, 0x89, 0x9b,
0x78, 0x59, 0xb2, 0x32, 0x82, 0x78, 0xec, 0x60, 0x92, 0x98, 0x2b, 0xf2, 0x84, 0x72, 0x34, 0xcb,
0x2c, 0xf4, 0x03, 0xb8, 0x93, 0x64, 0xf6, 0x9a, 0x99, 0xb1, 0xac, 0x5c, 0x4a, 0xf9, 0xc2, 0x2c,
0x6f, 0x51, 0x0e, 0x0a, 0x61, 0x66, 0x3f, 0xeb, 0x28, 0xfa, 0x39, 0xfb, 0x5b, 0xea, 0xc8, 0xf7,
0xa1, 0x50, 0x36, 0xb9, 0x4b, 0xac, 0xaf, 0x28, 0x36, 0x78, 0xc5, 0x9a, 0x90, 0xe7, 0xbc, 0x84,
0xb1, 0x37, 0xed, 0xd1, 0xf6, 0xb7, 0xe8, 0x8c, 0xe2, 0x9a, 0xb7, 0x0a, 0xf2, 0x23, 0xd8, 0xcf,
0x38, 0x68, 0x1f, 0x9a, 0x6f, 0xb4, 0xe9, 0x6b, 0x69, 0x0f, 0x09, 0xd0, 0xd2, 0x26, 0xca, 0x4b,
0x55, 0xaa, 0xc9, 0x7f, 0xa9, 0xc1, 0xfd, 0xea, 0x33, 0x34, 0x62, 0xed, 0x1d, 0x85, 0xef, 0x04,
0x60, 0x95, 0x5e, 0x7a, 0xae, 0x6d, 0x5e, 0xe3, 0x35, 0x0f, 0x8d, 0xc0, 0x38, 0xaf, 0xf1, 0x1a,
0x3d, 0x86, 0xae, 0xe3, 0xc6, 0x2b, 0xcf, 0x5a, 0x9b, 0xa5, 0xfb, 0x2f, 0x72, 0x1e, 0x4d, 0xb8,
0x0f, 0x03, 0xfb, 0x1c, 0x9a, 0xae, 0x1d, 0x06, 0x14, 0x4b, 0x71, 0x97, 0x5f, 0x79, 0xaa, 0xe8,
0x54, 0x58, 0xfe, 0x53, 0x1d, 0xbe, 0xb7, 0x69, 0xb6, 0xef, 0xa7, 0x81, 0x9b, 0xac, 0xa9, 0xe1,
0x8f, 0x49, 0x23, 0xc0, 0x19, 0xa6, 0x9b, 0x65, 0x95, 0x98, 0xf3, 0x34, 0xe7, 0xff, 0x63, 0xfc,
0x13, 0xe8, 0xf9, 0xd8, 0xbf, 0xc4, 0x51, 0xd6, 0x14, 0xb2, 0x7e, 0xb2, 0xcb, 0x99, 0xb4, 0x21,
0xa4, 0x8f, 0x5a, 0xe8, 0x85, 0x11, 0x2f, 0x62, 0x8c, 0xc8, 0xfd, 0xee, 0x7c, 0x84, 0xdf, 0xe8,
0xa7, 0xd0, 0xbe, 0x24, 0xe5, 0x25, 0xa2, 0x77, 0xeb, 0x3b, 0xa8, 0x71, 0x71, 0x52, 0x71, 0x36,
0xe3, 0xcc, 0xea, 0x54, 0x0e, 0x17, 0x23, 0xcd, 0x34, 0xcd, 0x8b, 0x8f, 0xc8, 0x79, 0x8b, 0xd4,
0xa5, 0x17, 0x11, 0xfb, 0xe1, 0xaf, 0xdd, 0xec, 0xde, 0x50, 0xe2, 0x06, 0x88, 0x8d, 0x0f, 0x82,
0xd8, 0xbc, 0x09, 0xe2, 0x76, 0x7c, 0x46, 0x20, 0xe4, 0xe1, 0xe2, 0xef, 0xe7, 0xe7, 0xbb, 0x5a,
0xa7, 0x4a, 0xf0, 0xf5, 0x42, 0x4f, 0xfe, 0x6f, 0x0d, 0xd0, 0xcd, 0x2e, 0x6b, 0x4b, 0x59, 0xf8,
0x8a, 0x3d, 0xe2, 0x96, 0xcd, 0x1a, 0x5b, 0xf1, 0xfc, 0xc9, 0xee, 0xb3, 0xf2, 0xfb, 0xf1, 0x6a,
0x4f, 0xcf, 0xb4, 0x90, 0x5a, 0x36, 0xb7, 0xf1, 0x11, 0xe6, 0xbe, 0xda, 0x2b, 0x19, 0x4c, 0xed,
0x60, 0xa8, 0xf3, 0x0b, 0xb1, 0xdb, 0x8e, 0x22, 0x7e, 0xd4, 0x0e, 0x46, 0xbe, 0x10, 0xf2, 0x52,
0x2a, 0x5f, 0xc1, 0xe1, 0x96, 0x0e, 0x13, 0xcd, 0x76, 0xf5, 0xa7, 0x35, 0xda, 0x26, 0x3c, 0xbc,
0xad, 0x3f, 0xdd, 0xde, 0x9e, 0xfe, 0x13, 0x40, 0x2c, 0x75, 0xd6, 0x3b, 0xba, 0xb8, 0x4a, 0xbf,
0x55, 0xa7, 0x2b, 0xa5, 0x7e, 0x2b, 0x1b, 0x2b, 0x1a, 0xa5, 0xb1, 0xe2, 0x11, 0x88, 0x11, 0x8e,
0x57, 0x61, 0x10, 0x63, 0x33, 0x09, 0x79, 0xea, 0x40, 0xc6, 0x32, 0x42, 0x32, 0xe1, 0xe1, 0x20,
0x66, 0xa9, 0xc7, 0x7b, 0x2f, 0x1c, 0xc4, 0x34, 0xed, 0x4a, 0xad, 0x70, 0xbb, 0xd2, 0x0a, 0x6f,
0x76, 0xb5, 0x9d, 0x4f, 0x9e, 0x2e, 0xf6, 0x3f, 0x69, 0xba, 0xf8, 0x02, 0x3a, 0x31, 0x9b, 0x91,
0xf9, 0x40, 0x30, 0x28, 0x36, 0xa8, 0x0e, 0xcf, 0x24, 0xac, 0x5c, 0x14, 0x9d, 0x41, 0x8b, 0x0e,
0x9f, 0x03, 0xa0, 0x3a, 0xc7, 0x1b, 0x53, 0x6f, 0xa1, 0xc1, 0xc4, 0x88, 0xbc, 0x45, 0x46, 0xc0,
0x81, 0xb8, 0x29, 0x5f, 0x1e, 0x2d, 0x89, 0x3c, 0x15, 0x43, 0x9f, 0x95, 0xd3, 0xb7, 0x4b, 0xaa,
0x65, 0x35, 0x2f, 0x47, 0x70, 0xe0, 0xb0, 0x86, 0x25, 0xfb, 0x97, 0x60, 0x60, 0x6f, 0x5a, 0x5f,
0xed, 0x68, 0x5e, 0xed, 0xe9, 0x7d, 0xa7, 0xda, 0x95, 0xe7, 0x63, 0x46, 0xaf, 0x3c, 0x66, 0x6c,
0xd6, 0x90, 0xfe, 0xcd, 0x1a, 0xb2, 0x82, 0x21, 0xbf, 0x67, 0x66, 0x84, 0x7f, 0x93, 0xe2, 0x38,
0x31, 0x57, 0x51, 0xb8, 0xb2, 0xae, 0xac, 0x84, 0x27, 0x31, 0x1e, 0x1c, 0x50, 0x73, 0x9e, 0x95,
0xa2, 0xc1, 0x34, 0x74, 0xa6, 0x30, 0xcb, 0xe5, 0x49, 0xe6, 0x62, 0xfd, 0xc4, 0xbe, 0x6d, 0x79,
0xcb, 0x0c, 0x28, 0x7d, 0xcc, 0x0c, 0xf8, 0x39, 0xb4, 0xe8, 0xff, 0x22, 0x83, 0x3b, 0xd4, 0xaa,
0x83, 0x52, 0x88, 0x09, 0x5b, 0x67, 0xab, 0xbb, 0x47, 0x45, 0xf4, 0xc9, 0xa3, 0xe2, 0xdf, 0x1a,
0x20, 0x8e, 0x2a, 0x9d, 0xda, 0x51, 0x36, 0xfe, 0x8f, 0x2e, 0xa6, 0x86, 0x3a, 0x35, 0xb2, 0x3f,
0x00, 0xfa, 0x00, 0x86, 0xfa, 0x4b, 0xc3, 0x9c, 0xbd, 0x51, 0xb4, 0xa9, 0x54, 0x43, 0x22, 0x74,
0xe6, 0x86, 0x36, 0x7a, 0xad, 0xea, 0x52, 0x1d, 0x01, 0xb4, 0xe7, 0x86, 0x62, 0x2c, 0xe6, 0x52,
0x83, 0xb4, 0x0b, 0xea, 0xe4, 0xe2, 0x17, 0x9a, 0xd4, 0x44, 0xf7, 0xe0, 0xd0, 0xd0, 0x95, 0xe9,
0x5c, 0x19, 0x19, 0xda, 0x05, 0xd9, 0x71, 0x32, 0x51, 0xa6, 0x63, 0xa9, 0x85, 0x4e, 0xe1, 0xe9,
0xfc, 0xdd, 0xdc, 0x50, 0x27, 0xe6, 0x44, 0x9d, 0xcf, 0x95, 0x97, 0x6a, 0x7e, 0xda, 0x4c, 0xd7,
0xde, 0x2a, 0x86, 0x6a, 0xbe, 0xd4, 0x2f, 0x16, 0x33, 0xa9, 0x5d, 0x34, 0x1f, 0x1d, 0xf2, 0x49,
0xff, 0x92, 0x90, 0xf6, 0x51, 0x0f, 0x04, 0xb2, 0xd9, 0x62, 0xaa, 0x19, 0xef, 0x24, 0x01, 0x1d,
0x03, 0xda, 0xd8, 0xee, 0xa5, 0x32, 0x93, 0x00, 0x1d, 0xc2, 0x01, 0xd9, 0x57, 0x19, 0x19, 0xa6,
0xae, 0x7e, 0xb3, 0x50, 0xe7, 0x86, 0x24, 0x12, 0xe6, 0x58, 0x9b, 0x8f, 0x2e, 0xf4, 0x71, 0x26,
0x2d, 0x75, 0xd1, 0x7d, 0xb8, 0xab, 0x8d, 0xd5, 0xa9, 0xa1, 0x19, 0xef, 0xcc, 0xb7, 0xaa, 0xae,
0x7d, 0xad, 0x8d, 0x14, 0x62, 0xb3, 0xd4, 0x43, 0x8f, 0xe1, 0x64, 0x63, 0xf3, 0x99, 0x36, 0x9d,
0xaa, 0x85, 0x76, 0x1f, 0x3d, 0x85, 0xe1, 0x86, 0xc8, 0x64, 0x61, 0x2c, 0x94, 0x37, 0xa6, 0xfa,
0x96, 0xf8, 0x34, 0x57, 0xa7, 0x86, 0x74, 0xb0, 0xc5, 0xe9, 0x8a, 0x94, 0x32, 0x1a, 0xa9, 0x33,
0x43, 0x1d, 0x4b, 0x12, 0x7a, 0x06, 0x4f, 0x6e, 0x93, 0xd4, 0xd5, 0xc9, 0xc5, 0x5b, 0x75, 0x2c,
0xdd, 0x29, 0x95, 0xef, 0x17, 0xbd, 0x5f, 0x89, 0x67, 0x3f, 0xfa, 0x59, 0x96, 0x01, 0x97, 0x6d,
0xfa, 0xf5, 0xfc, 0x7f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x2b, 0xe5, 0x53, 0x73, 0xab, 0x13, 0x00,
0x00,
}