Skip to content

Commit

Permalink
feat: introduce messenger APIs to extract discord channels
Browse files Browse the repository at this point in the history
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: status-im/status-desktop#6690
  • Loading branch information
0x-r4bbit committed Aug 4, 2022
1 parent 0135cc1 commit 9c568c5
Show file tree
Hide file tree
Showing 22 changed files with 1,270 additions and 613 deletions.
335 changes: 194 additions & 141 deletions appdatabase/migrations/bindata.go

Large diffs are not rendered by default.

576 changes: 328 additions & 248 deletions appdatabase/migrationsprevnodecfg/bindata.go

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions mailserver/migrations/bindata.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 10 additions & 10 deletions multiaccounts/migrations/bindata.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions protocol/anonmetrics/migrations/migrations.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

85 changes: 85 additions & 0 deletions protocol/communities_messenger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"io/ioutil"
"os"
"strings"
"testing"
"time"
Expand All @@ -25,6 +26,7 @@ import (
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/communities"
"github.com/status-im/status-go/protocol/discord"
"github.com/status-im/status-go/protocol/encryption/multidevice"
"github.com/status-im/status-go/protocol/protobuf"
"github.com/status-im/status-go/protocol/requests"
Expand Down Expand Up @@ -1975,3 +1977,86 @@ func (s *MessengerCommunitiesSuite) TestSetMutePropertyOnChatsByCategory() {
s.Require().False(chat.Muted)
}
}

func (s *MessengerCommunitiesSuite) TestExtractDiscordChannelsAndCategories() {

tmpFile, err := ioutil.TempFile(os.TempDir(), "discord-channel-")
s.Require().NoError(err)
defer os.Remove(tmpFile.Name())

discordMessage := &protobuf.DiscordMessage{
Id: "1234",
Type: "Default",
Timestamp: "2022-07-26T14:20:17.305+00:00",
TimestampEdited: "",
Content: "Some discord message",
Author: &protobuf.DiscordMessageAuthor{
Id: "123",
Name: "TestAuthor",
Discriminator: "456",
Nickname: "",
AvatarUrl: "",
},
}

messages := make([]*protobuf.DiscordMessage, 0)
messages = append(messages, discordMessage)

exportedDiscordData := &discord.ExportedData{
Channel: discord.Channel{
ID: "12345",
CategoryName: "test-category",
CategoryID: "6789",
Name: "test-channel",
Description: "This is a channel topic",
FilePath: tmpFile.Name(),
},
Messages: messages,
}

data, err := json.Marshal(exportedDiscordData)
s.Require().NoError(err)

err = os.WriteFile(tmpFile.Name(), data, 0666) // nolint: gosec
s.Require().NoError(err)

files := make([]string, 0)
files = append(files, tmpFile.Name())
mr, errs := s.bob.ExtractDiscordChannelsAndCategories(files)
s.Require().Len(errs, 0)

s.Require().Len(mr.DiscordCategories, 1)
s.Require().Len(mr.DiscordChannels, 1)
s.Require().Equal(mr.DiscordOldestMessageTimestamp, int(1658845217))
}

func (s *MessengerCommunitiesSuite) TestExtractDiscordChannelsAndCategories_WithErrors() {

tmpFile, err := ioutil.TempFile(os.TempDir(), "discord-channel-2")
s.Require().NoError(err)
defer os.Remove(tmpFile.Name())

exportedDiscordData := &discord.ExportedData{
Channel: discord.Channel{
ID: "12345",
CategoryName: "test-category",
CategoryID: "6789",
Name: "test-channel",
Description: "This is a channel topic",
FilePath: tmpFile.Name(),
},
Messages: make([]*protobuf.DiscordMessage, 0),
}

data, err := json.Marshal(exportedDiscordData)
s.Require().NoError(err)

err = os.WriteFile(tmpFile.Name(), data, 0666) // nolint: gosec
s.Require().NoError(err)

files := make([]string, 0)
files = append(files, tmpFile.Name())
_, errs := s.bob.ExtractDiscordChannelsAndCategories(files)
// Expecting 1 errors since there are no messages to be extracted
s.Require().Len(errs, 1)
}
59 changes: 59 additions & 0 deletions protocol/discord/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package discord

import (
"fmt"

"github.com/status-im/status-go/protocol/protobuf"
)

type ErrorType uint

const (
NoError ErrorType = iota
Warning
Error
)

type Channel struct {
ID string `json:"id"`
CategoryName string `json:"category"`
CategoryID string `json:"categoryId"`
Name string `json:"name"`
Description string `json:"topic"`
FilePath string `json:"filePath"`
}

type Category struct {
ID string `json:"id"`
Name string `json:"name"`
}

type ExportedData struct {
Channel Channel `json:"channel"`
Messages []*protobuf.DiscordMessage `json:"messages"`
}

type ExtractedData struct {
Categories map[string]*Category
ExportedData []*ExportedData
OldestMessageTimestamp int
}

type ImportError struct {
// This code is used to distinguish between errors
// that are considered "criticial" and those that are not.
//
// Critical errors are the ones that prevent the imported community
// from functioning properly. For example, if the creation of the community
// or its categories and channels fails, this is a critical error.
//
// Non-critical errors are the ones that would not prevent the imported
// community from functioning. For example, if the channel data to be imported
// has no messages, or is not parsable.
Code ErrorType `json:"code"`
Message string `json:"message"`
}

func (d ImportError) Error() string {
return fmt.Sprintf("%d: %s", d.Code, d.Message)
}
Loading

0 comments on commit 9c568c5

Please sign in to comment.