github.com/status-im/status-go@v1.1.0/protocol/messenger_communities_import_discord.go (about)

     1  package protocol
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/golang/protobuf/proto"
    13  	"github.com/meirf/gopart"
    14  	"go.uber.org/zap"
    15  
    16  	"github.com/status-im/status-go/eth-node/crypto"
    17  	"github.com/status-im/status-go/eth-node/types"
    18  	"github.com/status-im/status-go/images"
    19  	"github.com/status-im/status-go/protocol/common"
    20  	"github.com/status-im/status-go/protocol/communities"
    21  	"github.com/status-im/status-go/protocol/discord"
    22  	"github.com/status-im/status-go/protocol/protobuf"
    23  	"github.com/status-im/status-go/protocol/requests"
    24  	"github.com/status-im/status-go/protocol/transport"
    25  	v1protocol "github.com/status-im/status-go/protocol/v1"
    26  )
    27  
    28  func (m *Messenger) ExtractDiscordDataFromImportFiles(filesToImport []string) (*discord.ExtractedData, map[string]*discord.ImportError) {
    29  
    30  	extractedData := &discord.ExtractedData{
    31  		Categories:             map[string]*discord.Category{},
    32  		ExportedData:           make([]*discord.ExportedData, 0),
    33  		OldestMessageTimestamp: 0,
    34  		MessageCount:           0,
    35  	}
    36  
    37  	errors := map[string]*discord.ImportError{}
    38  
    39  	for _, fileToImport := range filesToImport {
    40  		filePath := strings.Replace(fileToImport, "file://", "", -1)
    41  
    42  		fileInfo, err := os.Stat(filePath)
    43  		if err != nil {
    44  			errors[fileToImport] = discord.Error(err.Error())
    45  			continue
    46  		}
    47  
    48  		fileSize := fileInfo.Size()
    49  		if fileSize > discord.MaxImportFileSizeBytes {
    50  			errors[fileToImport] = discord.Error(discord.ErrImportFileTooBig.Error())
    51  			continue
    52  		}
    53  
    54  		bytes, err := os.ReadFile(filePath)
    55  		if err != nil {
    56  			errors[fileToImport] = discord.Error(err.Error())
    57  			continue
    58  		}
    59  
    60  		var discordExportedData discord.ExportedData
    61  
    62  		err = json.Unmarshal(bytes, &discordExportedData)
    63  		if err != nil {
    64  			errors[fileToImport] = discord.Error(err.Error())
    65  			continue
    66  		}
    67  
    68  		if len(discordExportedData.Messages) == 0 {
    69  			errors[fileToImport] = discord.Error(discord.ErrNoMessageData.Error())
    70  			continue
    71  		}
    72  
    73  		discordExportedData.Channel.FilePath = filePath
    74  		categoryID := discordExportedData.Channel.CategoryID
    75  
    76  		discordCategory := discord.Category{
    77  			ID:   categoryID,
    78  			Name: discordExportedData.Channel.CategoryName,
    79  		}
    80  
    81  		_, ok := extractedData.Categories[categoryID]
    82  		if !ok {
    83  			extractedData.Categories[categoryID] = &discordCategory
    84  		}
    85  
    86  		extractedData.MessageCount = extractedData.MessageCount + discordExportedData.MessageCount
    87  		extractedData.ExportedData = append(extractedData.ExportedData, &discordExportedData)
    88  
    89  		if len(discordExportedData.Messages) > 0 {
    90  			msgTime, err := time.Parse(discordTimestampLayout, discordExportedData.Messages[0].Timestamp)
    91  			if err != nil {
    92  				m.logger.Error("failed to parse discord message timestamp", zap.Error(err))
    93  				continue
    94  			}
    95  
    96  			if extractedData.OldestMessageTimestamp == 0 || int(msgTime.Unix()) <= extractedData.OldestMessageTimestamp {
    97  				// Exported discord channel data already comes with `messages` being
    98  				// sorted, starting with the oldest, so we can safely rely on the first
    99  				// message
   100  				extractedData.OldestMessageTimestamp = int(msgTime.Unix())
   101  			}
   102  		}
   103  	}
   104  	return extractedData, errors
   105  }
   106  
   107  func (m *Messenger) ExtractDiscordChannelsAndCategories(filesToImport []string) (*MessengerResponse, map[string]*discord.ImportError) {
   108  
   109  	response := &MessengerResponse{}
   110  
   111  	extractedData, errs := m.ExtractDiscordDataFromImportFiles(filesToImport)
   112  
   113  	for _, category := range extractedData.Categories {
   114  		response.AddDiscordCategory(category)
   115  	}
   116  	for _, export := range extractedData.ExportedData {
   117  		response.AddDiscordChannel(&export.Channel)
   118  	}
   119  	if extractedData.OldestMessageTimestamp != 0 {
   120  		response.DiscordOldestMessageTimestamp = extractedData.OldestMessageTimestamp
   121  	}
   122  
   123  	return response, errs
   124  }
   125  
   126  func (m *Messenger) RequestExtractDiscordChannelsAndCategories(filesToImport []string) {
   127  	go func() {
   128  		response, errors := m.ExtractDiscordChannelsAndCategories(filesToImport)
   129  		m.config.messengerSignalsHandler.DiscordCategoriesAndChannelsExtracted(
   130  			response.DiscordCategories,
   131  			response.DiscordChannels,
   132  			int64(response.DiscordOldestMessageTimestamp),
   133  			errors)
   134  	}()
   135  }
   136  func (m *Messenger) saveDiscordAuthorIfNotExists(discordAuthor *protobuf.DiscordMessageAuthor) *discord.ImportError {
   137  	exists, err := m.persistence.HasDiscordMessageAuthor(discordAuthor.GetId())
   138  	if err != nil {
   139  		m.logger.Error("failed to check if message author exists in database", zap.Error(err))
   140  		return discord.Error(err.Error())
   141  	}
   142  
   143  	if !exists {
   144  		err := m.persistence.SaveDiscordMessageAuthor(discordAuthor)
   145  		if err != nil {
   146  			return discord.Error(err.Error())
   147  		}
   148  	}
   149  
   150  	return nil
   151  }
   152  
   153  func (m *Messenger) convertDiscordMessageTimeStamp(discordMessage *protobuf.DiscordMessage, timestamp time.Time) *discord.ImportError {
   154  	discordMessage.Timestamp = fmt.Sprintf("%d", timestamp.Unix())
   155  
   156  	if discordMessage.TimestampEdited != "" {
   157  		timestampEdited, err := time.Parse(discordTimestampLayout, discordMessage.TimestampEdited)
   158  		if err != nil {
   159  			m.logger.Error("failed to parse discord message timestamp", zap.Error(err))
   160  			return discord.Warning(err.Error())
   161  		}
   162  		// Convert timestamp to unix timestamp
   163  		discordMessage.TimestampEdited = fmt.Sprintf("%d", timestampEdited.Unix())
   164  	}
   165  
   166  	return nil
   167  }
   168  
   169  func (m *Messenger) createPinMessageFromDiscordMessage(message *common.Message, pinnedMessageID string, channelID string, community *communities.Community) (*common.PinMessage, *discord.ImportError) {
   170  	pinMessage := protobuf.PinMessage{
   171  		Clock:       message.WhisperTimestamp,
   172  		MessageId:   pinnedMessageID,
   173  		ChatId:      message.LocalChatID,
   174  		MessageType: protobuf.MessageType_COMMUNITY_CHAT,
   175  		Pinned:      true,
   176  	}
   177  
   178  	encodedPayload, err := proto.Marshal(&pinMessage)
   179  	if err != nil {
   180  		m.logger.Error("failed to parse marshal pin message", zap.Error(err))
   181  		return nil, discord.Warning(err.Error())
   182  	}
   183  
   184  	wrappedPayload, err := v1protocol.WrapMessageV1(encodedPayload, protobuf.ApplicationMetadataMessage_PIN_MESSAGE, community.PrivateKey())
   185  	if err != nil {
   186  		m.logger.Error("failed to wrap pin message", zap.Error(err))
   187  		return nil, discord.Warning(err.Error())
   188  	}
   189  
   190  	pinMessageToSave := &common.PinMessage{
   191  		ID:               types.EncodeHex(v1protocol.MessageID(&community.PrivateKey().PublicKey, wrappedPayload)),
   192  		PinMessage:       &pinMessage,
   193  		LocalChatID:      channelID,
   194  		From:             message.From,
   195  		SigPubKey:        message.SigPubKey,
   196  		WhisperTimestamp: message.WhisperTimestamp,
   197  	}
   198  
   199  	return pinMessageToSave, nil
   200  }
   201  
   202  func (m *Messenger) processDiscordMessages(discordChannel *discord.ExportedData,
   203  	channel *Chat,
   204  	importProgress *discord.ImportProgress,
   205  	progressUpdates chan *discord.ImportProgress,
   206  	fromDate int64,
   207  	community *communities.Community) (
   208  	map[string]*common.Message,
   209  	[]*common.PinMessage,
   210  	map[string]*protobuf.DiscordMessageAuthor,
   211  	[]*protobuf.DiscordMessageAttachment) {
   212  
   213  	messagesToSave := make(map[string]*common.Message, 0)
   214  	pinMessagesToSave := make([]*common.PinMessage, 0)
   215  	authorProfilesToSave := make(map[string]*protobuf.DiscordMessageAuthor, 0)
   216  	messageAttachmentsToDownload := make([]*protobuf.DiscordMessageAttachment, 0)
   217  
   218  	for _, discordMessage := range discordChannel.Messages {
   219  
   220  		timestamp, err := time.Parse(discordTimestampLayout, discordMessage.Timestamp)
   221  		if err != nil {
   222  			m.logger.Error("failed to parse discord message timestamp", zap.Error(err))
   223  			importProgress.AddTaskError(discord.ImportMessagesTask, discord.Warning(err.Error()))
   224  			progressUpdates <- importProgress
   225  			continue
   226  		}
   227  
   228  		if timestamp.Unix() < fromDate {
   229  			progressUpdates <- importProgress
   230  			continue
   231  		}
   232  
   233  		importErr := m.saveDiscordAuthorIfNotExists(discordMessage.Author)
   234  		if importErr != nil {
   235  			importProgress.AddTaskError(discord.ImportMessagesTask, importErr)
   236  			progressUpdates <- importProgress
   237  			continue
   238  		}
   239  
   240  		hasPayload, err := m.persistence.HasDiscordMessageAuthorImagePayload(discordMessage.Author.GetId())
   241  		if err != nil {
   242  			m.logger.Error("failed to check if message avatar payload exists in database", zap.Error(err))
   243  			importProgress.AddTaskError(discord.ImportMessagesTask, discord.Error(err.Error()))
   244  			progressUpdates <- importProgress
   245  			continue
   246  		}
   247  
   248  		if !hasPayload {
   249  			authorProfilesToSave[discordMessage.Author.Id] = discordMessage.Author
   250  		}
   251  
   252  		// Convert timestamp to unix timestamp
   253  		importErr = m.convertDiscordMessageTimeStamp(discordMessage, timestamp)
   254  		if importErr != nil {
   255  			importProgress.AddTaskError(discord.ImportMessagesTask, importErr)
   256  			progressUpdates <- importProgress
   257  			continue
   258  		}
   259  
   260  		for i := range discordMessage.Attachments {
   261  			discordMessage.Attachments[i].MessageId = discordMessage.Id
   262  		}
   263  		messageAttachmentsToDownload = append(messageAttachmentsToDownload, discordMessage.Attachments...)
   264  
   265  		clockAndTimestamp := uint64(timestamp.Unix()) * 1000
   266  		communityPubKey := community.PrivateKey().PublicKey
   267  
   268  		chatMessage := protobuf.ChatMessage{
   269  			Timestamp:   clockAndTimestamp,
   270  			MessageType: protobuf.MessageType_COMMUNITY_CHAT,
   271  			ContentType: protobuf.ChatMessage_DISCORD_MESSAGE,
   272  			Clock:       clockAndTimestamp,
   273  			ChatId:      channel.ID,
   274  			Payload: &protobuf.ChatMessage_DiscordMessage{
   275  				DiscordMessage: discordMessage,
   276  			},
   277  		}
   278  
   279  		// Handle message replies
   280  		if discordMessage.Type == string(discord.MessageTypeReply) && discordMessage.Reference != nil {
   281  			repliedMessageID := community.IDString() + discordMessage.Reference.MessageId
   282  			if _, exists := messagesToSave[repliedMessageID]; exists {
   283  				chatMessage.ResponseTo = repliedMessageID
   284  			}
   285  		}
   286  
   287  		messageToSave := &common.Message{
   288  			ID:               community.IDString() + discordMessage.Id,
   289  			WhisperTimestamp: clockAndTimestamp,
   290  			From:             types.EncodeHex(crypto.FromECDSAPub(&communityPubKey)),
   291  			Seen:             true,
   292  			LocalChatID:      channel.ID,
   293  			SigPubKey:        &communityPubKey,
   294  			CommunityID:      community.IDString(),
   295  			ChatMessage:      &chatMessage,
   296  		}
   297  
   298  		err = messageToSave.PrepareContent(common.PubkeyToHex(&m.identity.PublicKey))
   299  		if err != nil {
   300  			m.logger.Error("failed to prepare message content", zap.Error(err))
   301  			importProgress.AddTaskError(discord.ImportMessagesTask, discord.Error(err.Error()))
   302  			progressUpdates <- importProgress
   303  			continue
   304  		}
   305  
   306  		// Handle pin messages
   307  		if discordMessage.Type == string(discord.MessageTypeChannelPinned) && discordMessage.Reference != nil {
   308  
   309  			pinnedMessageID := community.IDString() + discordMessage.Reference.MessageId
   310  			_, exists := messagesToSave[pinnedMessageID]
   311  			if exists {
   312  				pinMessageToSave, importErr := m.createPinMessageFromDiscordMessage(messageToSave, pinnedMessageID, channel.ID, community)
   313  				if importErr != nil {
   314  					importProgress.AddTaskError(discord.ImportMessagesTask, importErr)
   315  					progressUpdates <- importProgress
   316  					continue
   317  				}
   318  
   319  				pinMessagesToSave = append(pinMessagesToSave, pinMessageToSave)
   320  
   321  				// Generate SystemMessagePinnedMessage
   322  				systemMessage, importErr := m.generateSystemPinnedMessage(pinMessageToSave, channel, clockAndTimestamp, pinnedMessageID)
   323  				if importErr != nil {
   324  					importProgress.AddTaskError(discord.ImportMessagesTask, importErr)
   325  					progressUpdates <- importProgress
   326  					continue
   327  				}
   328  
   329  				messagesToSave[systemMessage.ID] = systemMessage
   330  			}
   331  		} else {
   332  			messagesToSave[messageToSave.ID] = messageToSave
   333  		}
   334  	}
   335  
   336  	return messagesToSave, pinMessagesToSave, authorProfilesToSave, messageAttachmentsToDownload
   337  }
   338  
   339  func calculateProgress(i int, t int, currentProgress float32) float32 {
   340  	current := float32(1) / float32(t) * currentProgress
   341  	if i > 1 {
   342  		return float32(i-1)/float32(t) + current
   343  	}
   344  	return current
   345  }
   346  
   347  func (m *Messenger) MarkDiscordCommunityImportAsCancelled(communityID string) {
   348  	m.importingCommunities[communityID] = true
   349  }
   350  
   351  func (m *Messenger) MarkDiscordChannelImportAsCancelled(channelID string) {
   352  	m.importingChannels[channelID] = true
   353  }
   354  
   355  func (m *Messenger) DiscordImportMarkedAsCancelled(communityID string) bool {
   356  	cancelled, exists := m.importingCommunities[communityID]
   357  	return exists && cancelled
   358  }
   359  
   360  func (m *Messenger) DiscordImportChannelMarkedAsCancelled(channelID string) bool {
   361  	cancelled, exists := m.importingChannels[channelID]
   362  	return exists && cancelled
   363  }
   364  
   365  func (m *Messenger) cleanUpImports() {
   366  	for id := range m.importingCommunities {
   367  		m.cleanUpImport(id)
   368  	}
   369  }
   370  
   371  func (m *Messenger) cleanUpImport(communityID string) {
   372  	community, err := m.communitiesManager.GetByIDString(communityID)
   373  	if err != nil {
   374  		m.logger.Error("clean up failed, couldn't delete community", zap.Error(err))
   375  		return
   376  	}
   377  	deleteErr := m.communitiesManager.DeleteCommunity(community.ID())
   378  	if deleteErr != nil {
   379  		m.logger.Error("clean up failed, couldn't delete community", zap.Error(deleteErr))
   380  	}
   381  	deleteErr = m.persistence.DeleteMessagesByCommunityID(community.IDString())
   382  	if deleteErr != nil {
   383  		m.logger.Error("clean up failed, couldn't delete community messages", zap.Error(deleteErr))
   384  	}
   385  	m.config.messengerSignalsHandler.DiscordCommunityImportCleanedUp(communityID)
   386  }
   387  
   388  func (m *Messenger) cleanUpImportChannel(communityID string, channelID string) {
   389  	_, err := m.DeleteCommunityChat(types.HexBytes(communityID), channelID)
   390  	if err != nil {
   391  		m.logger.Error("clean up failed, couldn't delete community chat", zap.Error(err))
   392  		return
   393  	}
   394  
   395  	err = m.persistence.DeleteMessagesByChatID(channelID)
   396  	if err != nil {
   397  		m.logger.Error("clean up failed, couldn't delete community chat messages", zap.Error(err))
   398  		return
   399  	}
   400  }
   401  
   402  func (m *Messenger) publishImportProgress(progress *discord.ImportProgress) {
   403  	m.config.messengerSignalsHandler.DiscordCommunityImportProgress(progress)
   404  }
   405  
   406  func (m *Messenger) publishChannelImportProgress(progress *discord.ImportProgress) {
   407  	m.config.messengerSignalsHandler.DiscordChannelImportProgress(progress)
   408  }
   409  
   410  func (m *Messenger) startPublishImportProgressInterval(c chan *discord.ImportProgress, cancel chan string, done chan struct{}) {
   411  
   412  	var currentProgress *discord.ImportProgress
   413  
   414  	go func() {
   415  		ticker := time.NewTicker(2 * time.Second)
   416  		defer ticker.Stop()
   417  
   418  		for {
   419  			select {
   420  			case <-ticker.C:
   421  				if currentProgress != nil {
   422  					m.publishImportProgress(currentProgress)
   423  					if currentProgress.Stopped {
   424  						return
   425  					}
   426  				}
   427  			case progressUpdate := <-c:
   428  				currentProgress = progressUpdate
   429  			case <-done:
   430  				if currentProgress != nil {
   431  					m.publishImportProgress(currentProgress)
   432  				}
   433  				return
   434  			case communityID := <-cancel:
   435  				if currentProgress != nil {
   436  					m.publishImportProgress(currentProgress)
   437  				}
   438  				m.cleanUpImport(communityID)
   439  				m.config.messengerSignalsHandler.DiscordCommunityImportCancelled(communityID)
   440  				return
   441  			case <-m.quit:
   442  				m.cleanUpImports()
   443  				return
   444  			}
   445  		}
   446  	}()
   447  }
   448  
   449  func (m *Messenger) startPublishImportChannelProgressInterval(c chan *discord.ImportProgress, cancel chan []string, done chan struct{}) {
   450  
   451  	var currentProgress *discord.ImportProgress
   452  
   453  	go func() {
   454  		ticker := time.NewTicker(2 * time.Second)
   455  		defer ticker.Stop()
   456  
   457  		for {
   458  			select {
   459  			case <-ticker.C:
   460  				if currentProgress != nil {
   461  					m.publishChannelImportProgress(currentProgress)
   462  					if currentProgress.Stopped {
   463  						return
   464  					}
   465  				}
   466  			case progressUpdate := <-c:
   467  				currentProgress = progressUpdate
   468  			case <-done:
   469  				if currentProgress != nil {
   470  					m.publishChannelImportProgress(currentProgress)
   471  				}
   472  				return
   473  			case ids := <-cancel:
   474  				if currentProgress != nil {
   475  					m.publishImportProgress(currentProgress)
   476  				}
   477  				if len(ids) > 0 {
   478  					communityID := ids[0]
   479  					channelID := ids[1]
   480  					discordChannelID := ids[2]
   481  					m.cleanUpImportChannel(communityID, channelID)
   482  					m.config.messengerSignalsHandler.DiscordChannelImportCancelled(discordChannelID)
   483  				}
   484  				return
   485  			case <-m.quit:
   486  				m.cleanUpImports()
   487  				return
   488  			}
   489  		}
   490  	}()
   491  }
   492  func createCommunityChannelForImport(request *requests.ImportDiscordChannel) *protobuf.CommunityChat {
   493  	return &protobuf.CommunityChat{
   494  		Permissions: &protobuf.CommunityPermissions{
   495  			Access: protobuf.CommunityPermissions_AUTO_ACCEPT,
   496  		},
   497  		Identity: &protobuf.ChatIdentity{
   498  			DisplayName: request.Name,
   499  			Emoji:       request.Emoji,
   500  			Description: request.Description,
   501  			Color:       request.Color,
   502  		},
   503  		CategoryId:              "",
   504  		HideIfPermissionsNotMet: false,
   505  	}
   506  }
   507  
   508  func (m *Messenger) RequestImportDiscordChannel(request *requests.ImportDiscordChannel) {
   509  	go func() {
   510  		totalImportChunkCount := len(request.FilesToImport)
   511  
   512  		progressUpdates := make(chan *discord.ImportProgress)
   513  
   514  		done := make(chan struct{})
   515  		cancel := make(chan []string)
   516  
   517  		var newChat *Chat
   518  
   519  		m.startPublishImportChannelProgressInterval(progressUpdates, cancel, done)
   520  
   521  		importProgress := &discord.ImportProgress{}
   522  		importProgress.Init(totalImportChunkCount, []discord.ImportTask{
   523  			discord.ChannelsCreationTask,
   524  			discord.ImportMessagesTask,
   525  			discord.DownloadAssetsTask,
   526  			discord.InitCommunityTask,
   527  		})
   528  
   529  		importProgress.ChannelID = request.DiscordChannelID
   530  		importProgress.ChannelName = request.Name
   531  		// initial progress immediately
   532  
   533  		if err := request.Validate(); err != nil {
   534  			errmsg := fmt.Sprintf("Request validation failed: '%s'", err.Error())
   535  			importProgress.AddTaskError(discord.ChannelsCreationTask, discord.Error(errmsg))
   536  			importProgress.StopTask(discord.ChannelsCreationTask)
   537  			progressUpdates <- importProgress
   538  			cancel <- []string{request.CommunityID.String(), "", request.DiscordChannelID}
   539  			return
   540  		}
   541  
   542  		// Here's 3 steps: Find the corrent channel in files, get the community and create the channel
   543  		progressValue := float32(0.3)
   544  
   545  		m.publishChannelImportProgress(importProgress)
   546  
   547  		community, err := m.GetCommunityByID(request.CommunityID)
   548  
   549  		if err != nil {
   550  			errmsg := fmt.Sprintf("Couldn't get the community '%s': '%s'", request.CommunityID, err.Error())
   551  			importProgress.AddTaskError(discord.ChannelsCreationTask, discord.Error(errmsg))
   552  			importProgress.StopTask(discord.ChannelsCreationTask)
   553  			progressUpdates <- importProgress
   554  			cancel <- []string{request.CommunityID.String(), "", request.DiscordChannelID}
   555  			return
   556  		}
   557  
   558  		importProgress.UpdateTaskProgress(discord.ChannelsCreationTask, progressValue)
   559  		progressUpdates <- importProgress
   560  
   561  		for i, importFile := range request.FilesToImport {
   562  			m.importingChannels[request.DiscordChannelID] = false
   563  
   564  			exportData, errs := m.ExtractDiscordDataFromImportFiles([]string{importFile})
   565  			if len(errs) > 0 {
   566  				for _, err := range errs {
   567  					importProgress.AddTaskError(discord.ChannelsCreationTask, err)
   568  				}
   569  				importProgress.StopTask(discord.ChannelsCreationTask)
   570  				progressUpdates <- importProgress
   571  				cancel <- []string{request.CommunityID.String(), "", request.DiscordChannelID}
   572  				return
   573  			}
   574  
   575  			var channel *discord.ExportedData
   576  
   577  			for _, ch := range exportData.ExportedData {
   578  				if ch.Channel.ID == request.DiscordChannelID {
   579  					channel = ch
   580  				}
   581  			}
   582  
   583  			if channel == nil {
   584  				if i < len(request.FilesToImport)-1 {
   585  					// skip this file
   586  					continue
   587  				} else if i == len(request.FilesToImport)-1 {
   588  					errmsg := fmt.Sprintf("Couldn't find the target channel id in files: '%s'", request.DiscordChannelID)
   589  					importProgress.AddTaskError(discord.ChannelsCreationTask, discord.Error(errmsg))
   590  					importProgress.StopTask(discord.ChannelsCreationTask)
   591  					progressUpdates <- importProgress
   592  					cancel <- []string{request.CommunityID.String(), "", request.DiscordChannelID}
   593  					return
   594  				}
   595  			}
   596  			progressValue := float32(0.6)
   597  
   598  			importProgress.UpdateTaskProgress(discord.ChannelsCreationTask, progressValue)
   599  			progressUpdates <- importProgress
   600  
   601  			if m.DiscordImportChannelMarkedAsCancelled(request.DiscordChannelID) {
   602  				importProgress.StopTask(discord.ChannelsCreationTask)
   603  				progressUpdates <- importProgress
   604  				cancel <- []string{request.CommunityID.String(), "", request.DiscordChannelID}
   605  				return
   606  			}
   607  
   608  			if len(channel.Channel.ID) == 0 {
   609  				// skip this file and try to find in the next file
   610  				continue
   611  			}
   612  			exists := false
   613  
   614  			for _, chatID := range community.ChatIDs() {
   615  				if strings.HasSuffix(chatID, request.DiscordChannelID) {
   616  					exists = true
   617  					break
   618  				}
   619  			}
   620  
   621  			if !exists {
   622  				communityChat := createCommunityChannelForImport(request)
   623  
   624  				changes, err := m.communitiesManager.CreateChat(request.CommunityID, communityChat, false, channel.Channel.ID)
   625  				if err != nil {
   626  					errmsg := err.Error()
   627  					if errors.Is(err, communities.ErrInvalidCommunityDescriptionDuplicatedName) {
   628  						errmsg = fmt.Sprintf("Couldn't create channel '%s': %s", communityChat.Identity.DisplayName, err.Error())
   629  						fmt.Println(errmsg)
   630  					}
   631  
   632  					importProgress.AddTaskError(discord.ChannelsCreationTask, discord.Error(errmsg))
   633  					importProgress.StopTask(discord.ChannelsCreationTask)
   634  					progressUpdates <- importProgress
   635  					cancel <- []string{request.CommunityID.String(), "", request.DiscordChannelID}
   636  					return
   637  				}
   638  
   639  				community = changes.Community
   640  				for chatID, chat := range changes.ChatsAdded {
   641  					newChat = CreateCommunityChat(request.CommunityID.String(), chatID, chat, m.getTimesource())
   642  				}
   643  
   644  				progressValue = float32(1.0)
   645  
   646  				importProgress.UpdateTaskProgress(discord.ChannelsCreationTask, progressValue)
   647  				progressUpdates <- importProgress
   648  			} else {
   649  				// When channel with current discord id already exist we should skip import
   650  				importProgress.AddTaskError(discord.ChannelsCreationTask, discord.Error("Channel already imported to this community"))
   651  				importProgress.StopTask(discord.ChannelsCreationTask)
   652  				progressUpdates <- importProgress
   653  				cancel <- []string{request.CommunityID.String(), "", request.DiscordChannelID}
   654  				return
   655  			}
   656  
   657  			if m.DiscordImportChannelMarkedAsCancelled(request.DiscordChannelID) {
   658  				importProgress.StopTask(discord.ImportMessagesTask)
   659  				progressUpdates <- importProgress
   660  				cancel <- []string{request.CommunityID.String(), newChat.ID, request.DiscordChannelID}
   661  				return
   662  			}
   663  
   664  			messagesToSave, pinMessagesToSave, authorProfilesToSave, messageAttachmentsToDownload :=
   665  				m.processDiscordMessages(channel, newChat, importProgress, progressUpdates, request.From, community)
   666  
   667  			// If either there were no messages in the channel or something happened and all the messages errored, we
   668  			// we still up the percent to 100%
   669  			if len(messagesToSave) == 0 {
   670  				importProgress.UpdateTaskProgress(discord.ImportMessagesTask, 1.0)
   671  				progressUpdates <- importProgress
   672  			}
   673  
   674  			var discordMessages []*protobuf.DiscordMessage
   675  			for _, msg := range messagesToSave {
   676  				if msg.ChatMessage.ContentType == protobuf.ChatMessage_DISCORD_MESSAGE {
   677  					discordMessages = append(discordMessages, msg.GetDiscordMessage())
   678  				}
   679  			}
   680  
   681  			// We save these messages in chunks, so we don't block the database
   682  			// for a longer period of time
   683  			discordMessageChunks := chunkSlice(discordMessages, maxChunkSizeMessages)
   684  			chunksCount := len(discordMessageChunks)
   685  
   686  			for ii, msgs := range discordMessageChunks {
   687  				m.logger.Debug(fmt.Sprintf("saving %d/%d chunk with %d discord messages", ii+1, chunksCount, len(msgs)))
   688  				err := m.persistence.SaveDiscordMessages(msgs)
   689  				if err != nil {
   690  					m.cleanUpImportChannel(request.CommunityID.String(), newChat.ID)
   691  					importProgress.AddTaskError(discord.ImportMessagesTask, discord.Error(err.Error()))
   692  					importProgress.StopTask(discord.ImportMessagesTask)
   693  					progressUpdates <- importProgress
   694  					cancel <- []string{request.CommunityID.String(), newChat.ID, request.DiscordChannelID}
   695  					return
   696  				}
   697  
   698  				if m.DiscordImportChannelMarkedAsCancelled(request.DiscordChannelID) {
   699  					importProgress.StopTask(discord.ImportMessagesTask)
   700  					progressUpdates <- importProgress
   701  					cancel <- []string{request.CommunityID.String(), newChat.ID, request.DiscordChannelID}
   702  					return
   703  				}
   704  
   705  				// We're multiplying `chunksCount` by `0.25` so we leave 25% for additional save operations
   706  				// 0.5 are the previous 50% of progress
   707  				currentCount := ii + 1
   708  				progressValue := calculateProgress(i+1, totalImportChunkCount, 0.5+(float32(currentCount)/float32(chunksCount))*0.25)
   709  				importProgress.UpdateTaskProgress(discord.ImportMessagesTask, progressValue)
   710  				progressUpdates <- importProgress
   711  
   712  				// We slow down the saving of message chunks to keep the database responsive
   713  				if currentCount < chunksCount {
   714  					time.Sleep(2 * time.Second)
   715  				}
   716  			}
   717  
   718  			// Get slice of all values in `messagesToSave` map
   719  			var messages = make([]*common.Message, 0, len(messagesToSave))
   720  			for _, msg := range messagesToSave {
   721  				messages = append(messages, msg)
   722  			}
   723  
   724  			// Same as above, we save these messages in chunks so we don't block
   725  			// the database for a longer period of time
   726  			messageChunks := chunkSlice(messages, maxChunkSizeMessages)
   727  			chunksCount = len(messageChunks)
   728  
   729  			for ii, msgs := range messageChunks {
   730  				m.logger.Debug(fmt.Sprintf("saving %d/%d chunk with %d app messages", ii+1, chunksCount, len(msgs)))
   731  				err := m.persistence.SaveMessages(msgs)
   732  				if err != nil {
   733  					m.cleanUpImportChannel(request.CommunityID.String(), request.DiscordChannelID)
   734  					importProgress.AddTaskError(discord.ImportMessagesTask, discord.Error(err.Error()))
   735  					importProgress.StopTask(discord.ImportMessagesTask)
   736  					progressUpdates <- importProgress
   737  					cancel <- []string{request.CommunityID.String(), newChat.ID, request.DiscordChannelID}
   738  
   739  					return
   740  				}
   741  
   742  				if m.DiscordImportChannelMarkedAsCancelled(request.DiscordChannelID) {
   743  					importProgress.StopTask(discord.ImportMessagesTask)
   744  					progressUpdates <- importProgress
   745  					cancel <- []string{request.CommunityID.String(), newChat.ID, request.DiscordChannelID}
   746  					return
   747  				}
   748  
   749  				// 0.75 are the previous 75% of progress, hence we multiply our chunk progress
   750  				// by 0.25
   751  				currentCount := ii + 1
   752  				progressValue := calculateProgress(i+1, totalImportChunkCount, 0.75+(float32(currentCount)/float32(chunksCount))*0.25)
   753  				// progressValue := 0.75 + ((float32(currentCount) / float32(chunksCount)) * 0.25)
   754  				importProgress.UpdateTaskProgress(discord.ImportMessagesTask, progressValue)
   755  				progressUpdates <- importProgress
   756  
   757  				// We slow down the saving of message chunks to keep the database responsive
   758  				if currentCount < chunksCount {
   759  					time.Sleep(2 * time.Second)
   760  				}
   761  			}
   762  
   763  			pinMessageChunks := chunkSlice(pinMessagesToSave, maxChunkSizeMessages)
   764  			for _, pinMsgs := range pinMessageChunks {
   765  				err := m.persistence.SavePinMessages(pinMsgs)
   766  				if err != nil {
   767  					m.cleanUpImportChannel(request.CommunityID.String(), request.DiscordChannelID)
   768  					importProgress.AddTaskError(discord.ImportMessagesTask, discord.Error(err.Error()))
   769  					importProgress.StopTask(discord.ImportMessagesTask)
   770  					progressUpdates <- importProgress
   771  					cancel <- []string{request.CommunityID.String(), newChat.ID, request.DiscordChannelID}
   772  
   773  					return
   774  				}
   775  
   776  				if m.DiscordImportChannelMarkedAsCancelled(request.DiscordChannelID) {
   777  					importProgress.StopTask(discord.ImportMessagesTask)
   778  					progressUpdates <- importProgress
   779  					cancel <- []string{request.CommunityID.String(), newChat.ID, request.DiscordChannelID}
   780  					return
   781  				}
   782  			}
   783  
   784  			totalAssetsCount := len(messageAttachmentsToDownload) + len(authorProfilesToSave)
   785  			var assetCounter discord.AssetCounter
   786  
   787  			var wg sync.WaitGroup
   788  
   789  			for id, author := range authorProfilesToSave {
   790  				wg.Add(1)
   791  				go func(id string, author *protobuf.DiscordMessageAuthor) {
   792  					defer wg.Done()
   793  
   794  					m.logger.Debug(fmt.Sprintf("downloading asset %d/%d", assetCounter.Value()+1, totalAssetsCount))
   795  					imagePayload, err := discord.DownloadAvatarAsset(author.AvatarUrl)
   796  					if err != nil {
   797  						errmsg := fmt.Sprintf("Couldn't download profile avatar '%s': %s", author.AvatarUrl, err.Error())
   798  						importProgress.AddTaskError(
   799  							discord.DownloadAssetsTask,
   800  							discord.Warning(errmsg),
   801  						)
   802  						progressUpdates <- importProgress
   803  
   804  						return
   805  					}
   806  
   807  					err = m.persistence.UpdateDiscordMessageAuthorImage(author.Id, imagePayload)
   808  					if err != nil {
   809  						importProgress.AddTaskError(discord.DownloadAssetsTask, discord.Warning(err.Error()))
   810  						progressUpdates <- importProgress
   811  
   812  						return
   813  					}
   814  
   815  					author.AvatarImagePayload = imagePayload
   816  					authorProfilesToSave[id] = author
   817  
   818  					if m.DiscordImportMarkedAsCancelled(request.DiscordChannelID) {
   819  						importProgress.StopTask(discord.DownloadAssetsTask)
   820  						progressUpdates <- importProgress
   821  						cancel <- []string{request.CommunityID.String(), newChat.ID, request.DiscordChannelID}
   822  						return
   823  					}
   824  
   825  					assetCounter.Increase()
   826  					progressValue := calculateProgress(i+1, totalImportChunkCount, (float32(assetCounter.Value())/float32(totalAssetsCount))*0.5)
   827  					importProgress.UpdateTaskProgress(discord.DownloadAssetsTask, progressValue)
   828  					progressUpdates <- importProgress
   829  
   830  				}(id, author)
   831  			}
   832  			wg.Wait()
   833  
   834  			if m.DiscordImportChannelMarkedAsCancelled(request.DiscordChannelID) {
   835  				importProgress.StopTask(discord.DownloadAssetsTask)
   836  				progressUpdates <- importProgress
   837  				cancel <- []string{request.CommunityID.String(), newChat.ID, request.DiscordChannelID}
   838  				return
   839  			}
   840  
   841  			for idxRange := range gopart.Partition(len(messageAttachmentsToDownload), 100) {
   842  				attachments := messageAttachmentsToDownload[idxRange.Low:idxRange.High]
   843  				wg.Add(1)
   844  				go func(attachments []*protobuf.DiscordMessageAttachment) {
   845  					defer wg.Done()
   846  					for ii, attachment := range attachments {
   847  
   848  						m.logger.Debug(fmt.Sprintf("downloading asset %d/%d", assetCounter.Value()+1, totalAssetsCount))
   849  
   850  						assetPayload, contentType, err := discord.DownloadAsset(attachment.Url)
   851  						if err != nil {
   852  							errmsg := fmt.Sprintf("Couldn't download message attachment '%s': %s", attachment.Url, err.Error())
   853  							importProgress.AddTaskError(
   854  								discord.DownloadAssetsTask,
   855  								discord.Warning(errmsg),
   856  							)
   857  							progressUpdates <- importProgress
   858  							continue
   859  						}
   860  
   861  						attachment.Payload = assetPayload
   862  						attachment.ContentType = contentType
   863  						messageAttachmentsToDownload[ii] = attachment
   864  
   865  						if m.DiscordImportChannelMarkedAsCancelled(request.DiscordChannelID) {
   866  							importProgress.StopTask(discord.DownloadAssetsTask)
   867  							progressUpdates <- importProgress
   868  							cancel <- []string{request.CommunityID.String(), newChat.ID, request.DiscordChannelID}
   869  							return
   870  						}
   871  
   872  						assetCounter.Increase()
   873  						progressValue := calculateProgress(i+1, totalImportChunkCount, (float32(assetCounter.Value())/float32(totalAssetsCount))*0.5)
   874  						importProgress.UpdateTaskProgress(discord.DownloadAssetsTask, progressValue)
   875  						progressUpdates <- importProgress
   876  					}
   877  				}(attachments)
   878  			}
   879  			wg.Wait()
   880  
   881  			if m.DiscordImportChannelMarkedAsCancelled(request.DiscordChannelID) {
   882  				importProgress.StopTask(discord.DownloadAssetsTask)
   883  				progressUpdates <- importProgress
   884  				cancel <- []string{request.CommunityID.String(), newChat.ID, request.DiscordChannelID}
   885  				return
   886  			}
   887  
   888  			attachmentChunks := chunkAttachmentsByByteSize(messageAttachmentsToDownload, maxChunkSizeBytes)
   889  			chunksCount = len(attachmentChunks)
   890  
   891  			for ii, attachments := range attachmentChunks {
   892  				m.logger.Debug(fmt.Sprintf("saving %d/%d chunk with %d discord message attachments", ii+1, chunksCount, len(attachments)))
   893  				err := m.persistence.SaveDiscordMessageAttachments(attachments)
   894  				if err != nil {
   895  					importProgress.AddTaskError(discord.DownloadAssetsTask, discord.Warning(err.Error()))
   896  					importProgress.Stop()
   897  					progressUpdates <- importProgress
   898  
   899  					continue
   900  				}
   901  
   902  				if m.DiscordImportChannelMarkedAsCancelled(request.DiscordChannelID) {
   903  					importProgress.StopTask(discord.DownloadAssetsTask)
   904  					progressUpdates <- importProgress
   905  					cancel <- []string{request.CommunityID.String(), newChat.ID, request.DiscordChannelID}
   906  					return
   907  				}
   908  
   909  				// 0.5 are the previous 50% of progress, hence we multiply our chunk progress
   910  				// by 0.5
   911  				currentCount := ii + 1
   912  				progressValue := calculateProgress(i+1, totalImportChunkCount, 0.5+(float32(currentCount)/float32(chunksCount))*0.5)
   913  				importProgress.UpdateTaskProgress(discord.DownloadAssetsTask, progressValue)
   914  				progressUpdates <- importProgress
   915  
   916  				// We slow down the saving of attachment chunks to keep the database responsive
   917  				if currentCount < chunksCount {
   918  					time.Sleep(2 * time.Second)
   919  				}
   920  			}
   921  
   922  			if len(attachmentChunks) == 0 {
   923  				progressValue := calculateProgress(i+1, totalImportChunkCount, 1.0)
   924  				importProgress.UpdateTaskProgress(discord.DownloadAssetsTask, progressValue)
   925  			}
   926  
   927  			_, err := m.transport.JoinPublic(newChat.ID)
   928  			if err != nil {
   929  				m.logger.Error("failed to load filter for chat", zap.Error(err))
   930  				continue
   931  			}
   932  
   933  			wakuChatMessages, err := m.chatMessagesToWakuMessages(messages, community)
   934  			if err != nil {
   935  				m.logger.Error("failed to convert chat messages into waku messages", zap.Error(err))
   936  				continue
   937  			}
   938  
   939  			wakuPinMessages, err := m.pinMessagesToWakuMessages(pinMessagesToSave, community)
   940  			if err != nil {
   941  				m.logger.Error("failed to convert pin messages into waku messages", zap.Error(err))
   942  				continue
   943  			}
   944  
   945  			wakuMessages := append(wakuChatMessages, wakuPinMessages...)
   946  
   947  			topics, err := m.archiveManager.GetCommunityChatsTopics(request.CommunityID)
   948  			if err != nil {
   949  				m.logger.Error("failed to get community chat topics", zap.Error(err))
   950  				continue
   951  			}
   952  
   953  			startDate := time.Unix(int64(exportData.OldestMessageTimestamp), 0)
   954  			endDate := time.Now()
   955  
   956  			_, err = m.archiveManager.CreateHistoryArchiveTorrentFromMessages(
   957  				request.CommunityID,
   958  				wakuMessages,
   959  				topics,
   960  				startDate,
   961  				endDate,
   962  				messageArchiveInterval,
   963  				community.Encrypted(),
   964  			)
   965  			if err != nil {
   966  				m.logger.Error("failed to create history archive torrent", zap.Error(err))
   967  				continue
   968  			}
   969  			communitySettings, err := m.communitiesManager.GetCommunitySettingsByID(request.CommunityID)
   970  			if err != nil {
   971  				m.logger.Error("Failed to get community settings", zap.Error(err))
   972  				continue
   973  			}
   974  			if m.archiveManager.IsReady() && communitySettings.HistoryArchiveSupportEnabled {
   975  
   976  				err = m.archiveManager.SeedHistoryArchiveTorrent(request.CommunityID)
   977  				if err != nil {
   978  					m.logger.Error("failed to seed history archive", zap.Error(err))
   979  				}
   980  				go m.archiveManager.StartHistoryArchiveTasksInterval(community, messageArchiveInterval)
   981  			}
   982  		}
   983  
   984  		importProgress.UpdateTaskProgress(discord.InitCommunityTask, float32(0.0))
   985  
   986  		if m.DiscordImportChannelMarkedAsCancelled(request.DiscordChannelID) {
   987  			importProgress.StopTask(discord.InitCommunityTask)
   988  			progressUpdates <- importProgress
   989  			cancel <- []string{request.CommunityID.String(), newChat.ID, request.DiscordChannelID}
   990  			return
   991  		}
   992  
   993  		// Chats need to be saved after the community has been published,
   994  		// hence we make this part of the `InitCommunityTask`
   995  		err = m.saveChat(newChat)
   996  
   997  		if err != nil {
   998  			m.cleanUpImportChannel(request.CommunityID.String(), request.DiscordChannelID)
   999  			importProgress.AddTaskError(discord.InitCommunityTask, discord.Error(err.Error()))
  1000  			importProgress.Stop()
  1001  			progressUpdates <- importProgress
  1002  			cancel <- []string{request.CommunityID.String(), request.DiscordChannelID}
  1003  			return
  1004  		}
  1005  
  1006  		// Make sure all progress tasks are at 100%, in case one of the steps had errors
  1007  		// The front-end doesn't understand that the import is done until all tasks are at 100%
  1008  		importProgress.UpdateTaskProgress(discord.CommunityCreationTask, float32(1.0))
  1009  		importProgress.UpdateTaskProgress(discord.ChannelsCreationTask, float32(1.0))
  1010  		importProgress.UpdateTaskProgress(discord.ImportMessagesTask, float32(1.0))
  1011  		importProgress.UpdateTaskProgress(discord.DownloadAssetsTask, float32(1.0))
  1012  		importProgress.UpdateTaskProgress(discord.InitCommunityTask, float32(1.0))
  1013  
  1014  		m.config.messengerSignalsHandler.DiscordChannelImportFinished(request.CommunityID.String(), newChat.ID)
  1015  		close(done)
  1016  	}()
  1017  }
  1018  
  1019  func (m *Messenger) RequestImportDiscordCommunity(request *requests.ImportDiscordCommunity) {
  1020  	go func() {
  1021  
  1022  		totalImportChunkCount := len(request.FilesToImport)
  1023  
  1024  		progressUpdates := make(chan *discord.ImportProgress)
  1025  		done := make(chan struct{})
  1026  		cancel := make(chan string)
  1027  		m.startPublishImportProgressInterval(progressUpdates, cancel, done)
  1028  
  1029  		importProgress := &discord.ImportProgress{}
  1030  		importProgress.Init(totalImportChunkCount, []discord.ImportTask{
  1031  			discord.CommunityCreationTask,
  1032  			discord.ChannelsCreationTask,
  1033  			discord.ImportMessagesTask,
  1034  			discord.DownloadAssetsTask,
  1035  			discord.InitCommunityTask,
  1036  		})
  1037  		importProgress.CommunityName = request.Name
  1038  
  1039  		// initial progress immediately
  1040  		m.publishImportProgress(importProgress)
  1041  
  1042  		createCommunityRequest := request.ToCreateCommunityRequest()
  1043  
  1044  		// We're calling `CreateCommunity` on `communitiesManager` directly, instead of
  1045  		// using the `Messenger` API, so we get more control over when we set up filters,
  1046  		// the community is published and data is being synced (we don't want the community
  1047  		// to show up in clients while the import is in progress)
  1048  		discordCommunity, err := m.communitiesManager.CreateCommunity(createCommunityRequest, false)
  1049  		if err != nil {
  1050  			importProgress.AddTaskError(discord.CommunityCreationTask, discord.Error(err.Error()))
  1051  			importProgress.StopTask(discord.CommunityCreationTask)
  1052  			progressUpdates <- importProgress
  1053  			return
  1054  		}
  1055  
  1056  		communitySettings := communities.CommunitySettings{
  1057  			CommunityID:                  discordCommunity.IDString(),
  1058  			HistoryArchiveSupportEnabled: true,
  1059  		}
  1060  		err = m.communitiesManager.SaveCommunitySettings(communitySettings)
  1061  		if err != nil {
  1062  			m.cleanUpImport(discordCommunity.IDString())
  1063  			importProgress.AddTaskError(discord.CommunityCreationTask, discord.Error(err.Error()))
  1064  			importProgress.StopTask(discord.CommunityCreationTask)
  1065  			progressUpdates <- importProgress
  1066  			return
  1067  		}
  1068  
  1069  		communityID := discordCommunity.IDString()
  1070  
  1071  		// marking import as not cancelled
  1072  		m.importingCommunities[communityID] = false
  1073  		importProgress.CommunityID = communityID
  1074  		importProgress.CommunityImages = make(map[string]images.IdentityImage)
  1075  
  1076  		imgs := discordCommunity.Images()
  1077  		for t, i := range imgs {
  1078  			importProgress.CommunityImages[t] = images.IdentityImage{Name: t, Payload: i.Payload}
  1079  		}
  1080  
  1081  		importProgress.UpdateTaskProgress(discord.CommunityCreationTask, 1)
  1082  		progressUpdates <- importProgress
  1083  
  1084  		if m.DiscordImportMarkedAsCancelled(communityID) {
  1085  			importProgress.StopTask(discord.CommunityCreationTask)
  1086  			progressUpdates <- importProgress
  1087  			cancel <- communityID
  1088  			return
  1089  		}
  1090  
  1091  		var chatsToSave []*Chat
  1092  		createdChats := make(map[string]*Chat, 0)
  1093  		processedChannelIds := make(map[string]string, 0)
  1094  		processedCategoriesIds := make(map[string]string, 0)
  1095  
  1096  		// The map with counts of duplicated channel names
  1097  		uniqueChatNames := make(map[string]int, 0)
  1098  
  1099  		for i, importFile := range request.FilesToImport {
  1100  
  1101  			exportData, errs := m.ExtractDiscordDataFromImportFiles([]string{importFile})
  1102  			if len(errs) > 0 {
  1103  				for _, err := range errs {
  1104  					importProgress.AddTaskError(discord.CommunityCreationTask, err)
  1105  				}
  1106  				progressUpdates <- importProgress
  1107  				return
  1108  			}
  1109  			totalChannelsCount := len(exportData.ExportedData)
  1110  			totalMessageCount := exportData.MessageCount
  1111  
  1112  			if totalChannelsCount == 0 || totalMessageCount == 0 {
  1113  				importError := discord.Error(fmt.Errorf("No channel to import messages from in file: %s", importFile).Error())
  1114  				if totalMessageCount == 0 {
  1115  					importError.Message = fmt.Errorf("No messages to import in file: %s", importFile).Error()
  1116  				}
  1117  				importProgress.AddTaskError(discord.ChannelsCreationTask, importError)
  1118  				progressUpdates <- importProgress
  1119  				continue
  1120  			}
  1121  
  1122  			importProgress.CurrentChunk = i + 1
  1123  
  1124  			// We actually only ever receive a single category
  1125  			// from `exportData` but since it's a map, we still have to
  1126  			// iterate over it to access its values
  1127  			for _, category := range exportData.Categories {
  1128  
  1129  				categories := discordCommunity.Categories()
  1130  				exists := false
  1131  				for catID := range categories {
  1132  					if strings.HasSuffix(catID, category.ID) {
  1133  						exists = true
  1134  						break
  1135  					}
  1136  				}
  1137  
  1138  				if !exists {
  1139  					createCommunityCategoryRequest := &requests.CreateCommunityCategory{
  1140  						CommunityID:  discordCommunity.ID(),
  1141  						CategoryName: category.Name,
  1142  						ThirdPartyID: category.ID,
  1143  						ChatIDs:      make([]string, 0),
  1144  					}
  1145  					// We call `CreateCategory` on `communitiesManager` directly so we can control
  1146  					// whether or not the community update should be published (it should not until the
  1147  					// import has finished)
  1148  					communityWithCategories, changes, err := m.communitiesManager.CreateCategory(createCommunityCategoryRequest, false)
  1149  					if err != nil {
  1150  						m.cleanUpImport(communityID)
  1151  						importProgress.AddTaskError(discord.CommunityCreationTask, discord.Error(err.Error()))
  1152  						importProgress.StopTask(discord.CommunityCreationTask)
  1153  						progressUpdates <- importProgress
  1154  						return
  1155  					}
  1156  					discordCommunity = communityWithCategories
  1157  					// This looks like we keep overriding the same field but there's
  1158  					// only one `CategoriesAdded` change at this point.
  1159  					for _, addedCategory := range changes.CategoriesAdded {
  1160  						processedCategoriesIds[category.ID] = addedCategory.CategoryId
  1161  					}
  1162  				}
  1163  			}
  1164  
  1165  			progressValue := calculateProgress(i+1, totalImportChunkCount, (float32(1) / 2))
  1166  			importProgress.UpdateTaskProgress(discord.ChannelsCreationTask, progressValue)
  1167  
  1168  			progressUpdates <- importProgress
  1169  
  1170  			if m.DiscordImportMarkedAsCancelled(communityID) {
  1171  				importProgress.StopTask(discord.CommunityCreationTask)
  1172  				progressUpdates <- importProgress
  1173  				cancel <- communityID
  1174  				return
  1175  			}
  1176  
  1177  			messagesToSave := make(map[string]*common.Message, 0)
  1178  			pinMessagesToSave := make([]*common.PinMessage, 0)
  1179  			authorProfilesToSave := make(map[string]*protobuf.DiscordMessageAuthor, 0)
  1180  			messageAttachmentsToDownload := make([]*protobuf.DiscordMessageAttachment, 0)
  1181  
  1182  			// Save to access the first item here as we process
  1183  			// exported data by files which only holds a single channel
  1184  			channel := exportData.ExportedData[0]
  1185  			chatIDs := discordCommunity.ChatIDs()
  1186  
  1187  			exists := false
  1188  			for _, chatID := range chatIDs {
  1189  				if strings.HasSuffix(chatID, channel.Channel.ID) {
  1190  					exists = true
  1191  					break
  1192  				}
  1193  			}
  1194  
  1195  			if !exists {
  1196  				channelUniqueName := channel.Channel.Name
  1197  				if count, ok := uniqueChatNames[channelUniqueName]; ok {
  1198  					uniqueChatNames[channelUniqueName] = count + 1
  1199  					channelUniqueName = fmt.Sprintf("%s_%d", channelUniqueName, uniqueChatNames[channelUniqueName])
  1200  				} else {
  1201  					uniqueChatNames[channelUniqueName] = 1
  1202  				}
  1203  
  1204  				communityChat := &protobuf.CommunityChat{
  1205  					Permissions: &protobuf.CommunityPermissions{
  1206  						Access: protobuf.CommunityPermissions_AUTO_ACCEPT,
  1207  					},
  1208  					Identity: &protobuf.ChatIdentity{
  1209  						DisplayName: channelUniqueName,
  1210  						Emoji:       "",
  1211  						Description: channel.Channel.Description,
  1212  						Color:       discordCommunity.Color(),
  1213  					},
  1214  					CategoryId:              processedCategoriesIds[channel.Channel.CategoryID],
  1215  					HideIfPermissionsNotMet: false,
  1216  				}
  1217  
  1218  				// We call `CreateChat` on `communitiesManager` directly to get more control
  1219  				// over whether we want to publish the updated community description.
  1220  				changes, err := m.communitiesManager.CreateChat(discordCommunity.ID(), communityChat, false, channel.Channel.ID)
  1221  				if err != nil {
  1222  					m.cleanUpImport(communityID)
  1223  					errmsg := err.Error()
  1224  					if errors.Is(err, communities.ErrInvalidCommunityDescriptionDuplicatedName) {
  1225  						errmsg = fmt.Sprintf("Couldn't create channel '%s': %s", communityChat.Identity.DisplayName, err.Error())
  1226  					}
  1227  					importProgress.AddTaskError(discord.ChannelsCreationTask, discord.Error(errmsg))
  1228  					importProgress.StopTask(discord.ChannelsCreationTask)
  1229  					progressUpdates <- importProgress
  1230  					return
  1231  				}
  1232  				discordCommunity = changes.Community
  1233  
  1234  				// This looks like we keep overriding the chat id value
  1235  				// as we iterate over `ChatsAdded`, however at this point we
  1236  				// know there was only a single such change (and it's a map)
  1237  				for chatID, chat := range changes.ChatsAdded {
  1238  					c := CreateCommunityChat(communityID, chatID, chat, m.getTimesource())
  1239  					createdChats[c.ID] = c
  1240  					chatsToSave = append(chatsToSave, c)
  1241  					processedChannelIds[channel.Channel.ID] = c.ID
  1242  				}
  1243  			}
  1244  
  1245  			progressValue = calculateProgress(i+1, totalImportChunkCount, 1)
  1246  			importProgress.UpdateTaskProgress(discord.ChannelsCreationTask, progressValue)
  1247  			progressUpdates <- importProgress
  1248  
  1249  			for ii, discordMessage := range channel.Messages {
  1250  
  1251  				timestamp, err := time.Parse(discordTimestampLayout, discordMessage.Timestamp)
  1252  				if err != nil {
  1253  					m.logger.Error("failed to parse discord message timestamp", zap.Error(err))
  1254  					importProgress.AddTaskError(discord.ImportMessagesTask, discord.Warning(err.Error()))
  1255  					progressUpdates <- importProgress
  1256  					continue
  1257  				}
  1258  
  1259  				if timestamp.Unix() < request.From {
  1260  					progressUpdates <- importProgress
  1261  					continue
  1262  				}
  1263  
  1264  				exists, err := m.persistence.HasDiscordMessageAuthor(discordMessage.Author.GetId())
  1265  				if err != nil {
  1266  					m.logger.Error("failed to check if message author exists in database", zap.Error(err))
  1267  					importProgress.AddTaskError(discord.ImportMessagesTask, discord.Error(err.Error()))
  1268  					progressUpdates <- importProgress
  1269  					continue
  1270  				}
  1271  
  1272  				if !exists {
  1273  					err := m.persistence.SaveDiscordMessageAuthor(discordMessage.Author)
  1274  					if err != nil {
  1275  						importProgress.AddTaskError(discord.ImportMessagesTask, discord.Error(err.Error()))
  1276  						progressUpdates <- importProgress
  1277  						continue
  1278  					}
  1279  				}
  1280  
  1281  				hasPayload, err := m.persistence.HasDiscordMessageAuthorImagePayload(discordMessage.Author.GetId())
  1282  				if err != nil {
  1283  					m.logger.Error("failed to check if message avatar payload exists in database", zap.Error(err))
  1284  					importProgress.AddTaskError(discord.ImportMessagesTask, discord.Error(err.Error()))
  1285  					progressUpdates <- importProgress
  1286  					continue
  1287  				}
  1288  
  1289  				if !hasPayload {
  1290  					authorProfilesToSave[discordMessage.Author.Id] = discordMessage.Author
  1291  				}
  1292  
  1293  				// Convert timestamp to unix timestamp
  1294  				discordMessage.Timestamp = fmt.Sprintf("%d", timestamp.Unix())
  1295  
  1296  				if discordMessage.TimestampEdited != "" {
  1297  					timestampEdited, err := time.Parse(discordTimestampLayout, discordMessage.TimestampEdited)
  1298  					if err != nil {
  1299  						m.logger.Error("failed to parse discord message timestamp", zap.Error(err))
  1300  						importProgress.AddTaskError(discord.ImportMessagesTask, discord.Warning(err.Error()))
  1301  						progressUpdates <- importProgress
  1302  						continue
  1303  					}
  1304  					// Convert timestamp to unix timestamp
  1305  					discordMessage.TimestampEdited = fmt.Sprintf("%d", timestampEdited.Unix())
  1306  				}
  1307  
  1308  				for i := range discordMessage.Attachments {
  1309  					discordMessage.Attachments[i].MessageId = discordMessage.Id
  1310  				}
  1311  				messageAttachmentsToDownload = append(messageAttachmentsToDownload, discordMessage.Attachments...)
  1312  
  1313  				clockAndTimestamp := uint64(timestamp.Unix()) * 1000
  1314  				communityPubKey := discordCommunity.PrivateKey().PublicKey
  1315  
  1316  				chatMessage := protobuf.ChatMessage{
  1317  					Timestamp:   clockAndTimestamp,
  1318  					MessageType: protobuf.MessageType_COMMUNITY_CHAT,
  1319  					ContentType: protobuf.ChatMessage_DISCORD_MESSAGE,
  1320  					Clock:       clockAndTimestamp,
  1321  					ChatId:      processedChannelIds[channel.Channel.ID],
  1322  					Payload: &protobuf.ChatMessage_DiscordMessage{
  1323  						DiscordMessage: discordMessage,
  1324  					},
  1325  				}
  1326  
  1327  				// Handle message replies
  1328  				if discordMessage.Type == string(discord.MessageTypeReply) && discordMessage.Reference != nil {
  1329  					repliedMessageID := communityID + discordMessage.Reference.MessageId
  1330  					if _, exists := messagesToSave[repliedMessageID]; exists {
  1331  						chatMessage.ResponseTo = repliedMessageID
  1332  					}
  1333  				}
  1334  
  1335  				messageToSave := &common.Message{
  1336  					ID:               communityID + discordMessage.Id,
  1337  					WhisperTimestamp: clockAndTimestamp,
  1338  					From:             types.EncodeHex(crypto.FromECDSAPub(&communityPubKey)),
  1339  					Seen:             true,
  1340  					LocalChatID:      processedChannelIds[channel.Channel.ID],
  1341  					SigPubKey:        &communityPubKey,
  1342  					CommunityID:      communityID,
  1343  					ChatMessage:      &chatMessage,
  1344  				}
  1345  
  1346  				err = messageToSave.PrepareContent(common.PubkeyToHex(&m.identity.PublicKey))
  1347  				if err != nil {
  1348  					m.logger.Error("failed to prepare message content", zap.Error(err))
  1349  					importProgress.AddTaskError(discord.ImportMessagesTask, discord.Error(err.Error()))
  1350  					progressUpdates <- importProgress
  1351  					continue
  1352  				}
  1353  
  1354  				// Handle pin messages
  1355  				if discordMessage.Type == string(discord.MessageTypeChannelPinned) && discordMessage.Reference != nil {
  1356  
  1357  					pinnedMessageID := communityID + discordMessage.Reference.MessageId
  1358  					_, exists := messagesToSave[pinnedMessageID]
  1359  					if exists {
  1360  						pinMessage := protobuf.PinMessage{
  1361  							Clock:       messageToSave.WhisperTimestamp,
  1362  							MessageId:   pinnedMessageID,
  1363  							ChatId:      messageToSave.LocalChatID,
  1364  							MessageType: protobuf.MessageType_COMMUNITY_CHAT,
  1365  							Pinned:      true,
  1366  						}
  1367  
  1368  						encodedPayload, err := proto.Marshal(&pinMessage)
  1369  						if err != nil {
  1370  							m.logger.Error("failed to parse marshal pin message", zap.Error(err))
  1371  							importProgress.AddTaskError(discord.ImportMessagesTask, discord.Warning(err.Error()))
  1372  							progressUpdates <- importProgress
  1373  							continue
  1374  						}
  1375  
  1376  						wrappedPayload, err := v1protocol.WrapMessageV1(encodedPayload, protobuf.ApplicationMetadataMessage_PIN_MESSAGE, discordCommunity.PrivateKey())
  1377  						if err != nil {
  1378  							m.logger.Error("failed to wrap pin message", zap.Error(err))
  1379  							importProgress.AddTaskError(discord.ImportMessagesTask, discord.Warning(err.Error()))
  1380  							progressUpdates <- importProgress
  1381  							continue
  1382  						}
  1383  
  1384  						pinMessageToSave := common.PinMessage{
  1385  							ID:               types.EncodeHex(v1protocol.MessageID(&communityPubKey, wrappedPayload)),
  1386  							PinMessage:       &pinMessage,
  1387  							LocalChatID:      processedChannelIds[channel.Channel.ID],
  1388  							From:             messageToSave.From,
  1389  							SigPubKey:        messageToSave.SigPubKey,
  1390  							WhisperTimestamp: messageToSave.WhisperTimestamp,
  1391  						}
  1392  
  1393  						pinMessagesToSave = append(pinMessagesToSave, &pinMessageToSave)
  1394  
  1395  						// Generate SystemMessagePinnedMessage
  1396  
  1397  						chat, ok := createdChats[pinMessageToSave.LocalChatID]
  1398  						if !ok {
  1399  							err := errors.New("failed to get chat for pin message")
  1400  							m.logger.Warn(err.Error(),
  1401  								zap.String("PinMessageId", pinMessageToSave.ID),
  1402  								zap.String("ChatID", pinMessageToSave.LocalChatID))
  1403  							importProgress.AddTaskError(discord.ImportMessagesTask, discord.Warning(err.Error()))
  1404  							progressUpdates <- importProgress
  1405  							continue
  1406  						}
  1407  
  1408  						id, err := generatePinMessageNotificationID(&m.identity.PublicKey, &pinMessageToSave, chat)
  1409  						if err != nil {
  1410  							m.logger.Warn("failed to generate pin message notification ID",
  1411  								zap.String("PinMessageId", pinMessageToSave.ID))
  1412  							importProgress.AddTaskError(discord.ImportMessagesTask, discord.Warning(err.Error()))
  1413  							progressUpdates <- importProgress
  1414  							continue
  1415  						}
  1416  						systemMessage := &common.Message{
  1417  							ChatMessage: &protobuf.ChatMessage{
  1418  								Clock:       pinMessageToSave.Clock,
  1419  								Timestamp:   clockAndTimestamp,
  1420  								ChatId:      chat.ID,
  1421  								MessageType: pinMessageToSave.MessageType,
  1422  								ResponseTo:  pinMessage.MessageId,
  1423  								ContentType: protobuf.ChatMessage_SYSTEM_MESSAGE_PINNED_MESSAGE,
  1424  							},
  1425  							WhisperTimestamp: clockAndTimestamp,
  1426  							ID:               id,
  1427  							LocalChatID:      chat.ID,
  1428  							From:             messageToSave.From,
  1429  							Seen:             true,
  1430  						}
  1431  
  1432  						messagesToSave[systemMessage.ID] = systemMessage
  1433  					}
  1434  				} else {
  1435  					messagesToSave[messageToSave.ID] = messageToSave
  1436  				}
  1437  
  1438  				progressValue := calculateProgress(i+1, totalImportChunkCount, float32(ii+1)/float32(len(channel.Messages))*0.5)
  1439  				importProgress.UpdateTaskProgress(discord.ImportMessagesTask, progressValue)
  1440  				progressUpdates <- importProgress
  1441  			}
  1442  
  1443  			if m.DiscordImportMarkedAsCancelled(communityID) {
  1444  				importProgress.StopTask(discord.ImportMessagesTask)
  1445  				progressUpdates <- importProgress
  1446  				cancel <- communityID
  1447  				return
  1448  			}
  1449  
  1450  			var discordMessages []*protobuf.DiscordMessage
  1451  			for _, msg := range messagesToSave {
  1452  				if msg.ChatMessage.ContentType == protobuf.ChatMessage_DISCORD_MESSAGE {
  1453  					discordMessages = append(discordMessages, msg.GetDiscordMessage())
  1454  				}
  1455  			}
  1456  
  1457  			// We save these messages in chunks, so we don't block the database
  1458  			// for a longer period of time
  1459  			discordMessageChunks := chunkSlice(discordMessages, maxChunkSizeMessages)
  1460  			chunksCount := len(discordMessageChunks)
  1461  
  1462  			for ii, msgs := range discordMessageChunks {
  1463  				m.logger.Debug(fmt.Sprintf("saving %d/%d chunk with %d discord messages", ii+1, chunksCount, len(msgs)))
  1464  				err = m.persistence.SaveDiscordMessages(msgs)
  1465  				if err != nil {
  1466  					m.cleanUpImport(communityID)
  1467  					importProgress.AddTaskError(discord.ImportMessagesTask, discord.Error(err.Error()))
  1468  					importProgress.StopTask(discord.ImportMessagesTask)
  1469  					progressUpdates <- importProgress
  1470  					return
  1471  				}
  1472  
  1473  				if m.DiscordImportMarkedAsCancelled(communityID) {
  1474  					importProgress.StopTask(discord.ImportMessagesTask)
  1475  					progressUpdates <- importProgress
  1476  					cancel <- communityID
  1477  					return
  1478  				}
  1479  
  1480  				// We're multiplying `chunksCount` by `0.25` so we leave 25% for additional save operations
  1481  				// 0.5 are the previous 50% of progress
  1482  				currentCount := ii + 1
  1483  				progressValue := calculateProgress(i+1, totalImportChunkCount, 0.5+(float32(currentCount)/float32(chunksCount))*0.25)
  1484  				importProgress.UpdateTaskProgress(discord.ImportMessagesTask, progressValue)
  1485  				progressUpdates <- importProgress
  1486  
  1487  				// We slow down the saving of message chunks to keep the database responsive
  1488  				if currentCount < chunksCount {
  1489  					time.Sleep(2 * time.Second)
  1490  				}
  1491  			}
  1492  
  1493  			// Get slice of all values in `messagesToSave` map
  1494  
  1495  			var messages = make([]*common.Message, 0, len(messagesToSave))
  1496  			for _, msg := range messagesToSave {
  1497  				messages = append(messages, msg)
  1498  			}
  1499  
  1500  			// Same as above, we save these messages in chunks so we don't block
  1501  			// the database for a longer period of time
  1502  			messageChunks := chunkSlice(messages, maxChunkSizeMessages)
  1503  			chunksCount = len(messageChunks)
  1504  
  1505  			for ii, msgs := range messageChunks {
  1506  				m.logger.Debug(fmt.Sprintf("saving %d/%d chunk with %d app messages", ii+1, chunksCount, len(msgs)))
  1507  				err = m.persistence.SaveMessages(msgs)
  1508  				if err != nil {
  1509  					m.cleanUpImport(communityID)
  1510  					importProgress.AddTaskError(discord.ImportMessagesTask, discord.Error(err.Error()))
  1511  					importProgress.StopTask(discord.ImportMessagesTask)
  1512  					progressUpdates <- importProgress
  1513  					return
  1514  				}
  1515  
  1516  				if m.DiscordImportMarkedAsCancelled(communityID) {
  1517  					importProgress.StopTask(discord.ImportMessagesTask)
  1518  					progressUpdates <- importProgress
  1519  					cancel <- communityID
  1520  					return
  1521  				}
  1522  
  1523  				// 0.75 are the previous 75% of progress, hence we multiply our chunk progress
  1524  				// by 0.25
  1525  				currentCount := ii + 1
  1526  				progressValue := calculateProgress(i+1, totalImportChunkCount, 0.75+(float32(currentCount)/float32(chunksCount))*0.25)
  1527  				// progressValue := 0.75 + ((float32(currentCount) / float32(chunksCount)) * 0.25)
  1528  				importProgress.UpdateTaskProgress(discord.ImportMessagesTask, progressValue)
  1529  				progressUpdates <- importProgress
  1530  
  1531  				// We slow down the saving of message chunks to keep the database responsive
  1532  				if currentCount < chunksCount {
  1533  					time.Sleep(2 * time.Second)
  1534  				}
  1535  			}
  1536  
  1537  			pinMessageChunks := chunkSlice(pinMessagesToSave, maxChunkSizeMessages)
  1538  			for _, pinMsgs := range pinMessageChunks {
  1539  				err = m.persistence.SavePinMessages(pinMsgs)
  1540  				if err != nil {
  1541  					m.cleanUpImport(communityID)
  1542  					importProgress.AddTaskError(discord.ImportMessagesTask, discord.Error(err.Error()))
  1543  					importProgress.StopTask(discord.ImportMessagesTask)
  1544  					progressUpdates <- importProgress
  1545  					return
  1546  				}
  1547  
  1548  				if m.DiscordImportMarkedAsCancelled(communityID) {
  1549  					importProgress.StopTask(discord.ImportMessagesTask)
  1550  					progressUpdates <- importProgress
  1551  					cancel <- communityID
  1552  					return
  1553  				}
  1554  			}
  1555  
  1556  			totalAssetsCount := len(messageAttachmentsToDownload) + len(authorProfilesToSave)
  1557  			var assetCounter discord.AssetCounter
  1558  
  1559  			var wg sync.WaitGroup
  1560  
  1561  			for id, author := range authorProfilesToSave {
  1562  				wg.Add(1)
  1563  				go func(id string, author *protobuf.DiscordMessageAuthor) {
  1564  					defer wg.Done()
  1565  
  1566  					m.logger.Debug(fmt.Sprintf("downloading asset %d/%d", assetCounter.Value()+1, totalAssetsCount))
  1567  					imagePayload, err := discord.DownloadAvatarAsset(author.AvatarUrl)
  1568  					if err != nil {
  1569  						errmsg := fmt.Sprintf("Couldn't download profile avatar '%s': %s", author.AvatarUrl, err.Error())
  1570  						importProgress.AddTaskError(
  1571  							discord.DownloadAssetsTask,
  1572  							discord.Warning(errmsg),
  1573  						)
  1574  						progressUpdates <- importProgress
  1575  						return
  1576  					}
  1577  
  1578  					err = m.persistence.UpdateDiscordMessageAuthorImage(author.Id, imagePayload)
  1579  					if err != nil {
  1580  						importProgress.AddTaskError(discord.DownloadAssetsTask, discord.Warning(err.Error()))
  1581  						progressUpdates <- importProgress
  1582  						return
  1583  					}
  1584  
  1585  					author.AvatarImagePayload = imagePayload
  1586  					authorProfilesToSave[id] = author
  1587  
  1588  					if m.DiscordImportMarkedAsCancelled(discordCommunity.IDString()) {
  1589  						importProgress.StopTask(discord.DownloadAssetsTask)
  1590  						progressUpdates <- importProgress
  1591  						cancel <- discordCommunity.IDString()
  1592  						return
  1593  					}
  1594  
  1595  					assetCounter.Increase()
  1596  					progressValue := calculateProgress(i+1, totalImportChunkCount, (float32(assetCounter.Value())/float32(totalAssetsCount))*0.5)
  1597  					importProgress.UpdateTaskProgress(discord.DownloadAssetsTask, progressValue)
  1598  					progressUpdates <- importProgress
  1599  
  1600  				}(id, author)
  1601  			}
  1602  			wg.Wait()
  1603  
  1604  			if m.DiscordImportMarkedAsCancelled(communityID) {
  1605  				importProgress.StopTask(discord.DownloadAssetsTask)
  1606  				progressUpdates <- importProgress
  1607  				cancel <- communityID
  1608  				return
  1609  			}
  1610  
  1611  			for idxRange := range gopart.Partition(len(messageAttachmentsToDownload), 100) {
  1612  				attachments := messageAttachmentsToDownload[idxRange.Low:idxRange.High]
  1613  				wg.Add(1)
  1614  				go func(attachments []*protobuf.DiscordMessageAttachment) {
  1615  					defer wg.Done()
  1616  					for ii, attachment := range attachments {
  1617  
  1618  						m.logger.Debug(fmt.Sprintf("downloading asset %d/%d", assetCounter.Value()+1, totalAssetsCount))
  1619  
  1620  						assetPayload, contentType, err := discord.DownloadAsset(attachment.Url)
  1621  						if err != nil {
  1622  							errmsg := fmt.Sprintf("Couldn't download message attachment '%s': %s", attachment.Url, err.Error())
  1623  							importProgress.AddTaskError(
  1624  								discord.DownloadAssetsTask,
  1625  								discord.Warning(errmsg),
  1626  							)
  1627  							progressUpdates <- importProgress
  1628  							continue
  1629  						}
  1630  
  1631  						attachment.Payload = assetPayload
  1632  						attachment.ContentType = contentType
  1633  						messageAttachmentsToDownload[ii] = attachment
  1634  
  1635  						if m.DiscordImportMarkedAsCancelled(communityID) {
  1636  							importProgress.StopTask(discord.DownloadAssetsTask)
  1637  							progressUpdates <- importProgress
  1638  							cancel <- communityID
  1639  							return
  1640  						}
  1641  
  1642  						assetCounter.Increase()
  1643  						progressValue := calculateProgress(i+1, totalImportChunkCount, (float32(assetCounter.Value())/float32(totalAssetsCount))*0.5)
  1644  						importProgress.UpdateTaskProgress(discord.DownloadAssetsTask, progressValue)
  1645  						progressUpdates <- importProgress
  1646  					}
  1647  				}(attachments)
  1648  			}
  1649  			wg.Wait()
  1650  
  1651  			if m.DiscordImportMarkedAsCancelled(communityID) {
  1652  				importProgress.StopTask(discord.DownloadAssetsTask)
  1653  				progressUpdates <- importProgress
  1654  				cancel <- communityID
  1655  				return
  1656  			}
  1657  
  1658  			attachmentChunks := chunkAttachmentsByByteSize(messageAttachmentsToDownload, maxChunkSizeBytes)
  1659  			chunksCount = len(attachmentChunks)
  1660  
  1661  			for ii, attachments := range attachmentChunks {
  1662  				m.logger.Debug(fmt.Sprintf("saving %d/%d chunk with %d discord message attachments", ii+1, chunksCount, len(attachments)))
  1663  				err = m.persistence.SaveDiscordMessageAttachments(attachments)
  1664  				if err != nil {
  1665  					m.cleanUpImport(communityID)
  1666  					importProgress.AddTaskError(discord.DownloadAssetsTask, discord.Error(err.Error()))
  1667  					importProgress.Stop()
  1668  					progressUpdates <- importProgress
  1669  					return
  1670  				}
  1671  
  1672  				if m.DiscordImportMarkedAsCancelled(communityID) {
  1673  					importProgress.StopTask(discord.DownloadAssetsTask)
  1674  					progressUpdates <- importProgress
  1675  					cancel <- communityID
  1676  					return
  1677  				}
  1678  
  1679  				// 0.5 are the previous 50% of progress, hence we multiply our chunk progress
  1680  				// by 0.5
  1681  				currentCount := ii + 1
  1682  				progressValue := calculateProgress(i+1, totalImportChunkCount, 0.5+(float32(currentCount)/float32(chunksCount))*0.5)
  1683  				importProgress.UpdateTaskProgress(discord.DownloadAssetsTask, progressValue)
  1684  				progressUpdates <- importProgress
  1685  
  1686  				// We slow down the saving of attachment chunks to keep the database responsive
  1687  				if currentCount < chunksCount {
  1688  					time.Sleep(2 * time.Second)
  1689  				}
  1690  			}
  1691  
  1692  			if len(attachmentChunks) == 0 {
  1693  				progressValue := calculateProgress(i+1, totalImportChunkCount, 1.0)
  1694  				importProgress.UpdateTaskProgress(discord.DownloadAssetsTask, progressValue)
  1695  			}
  1696  
  1697  			_, err := m.transport.JoinPublic(processedChannelIds[channel.Channel.ID])
  1698  			if err != nil {
  1699  				m.logger.Error("failed to load filter for chat", zap.Error(err))
  1700  				continue
  1701  			}
  1702  
  1703  			wakuChatMessages, err := m.chatMessagesToWakuMessages(messages, discordCommunity)
  1704  			if err != nil {
  1705  				m.logger.Error("failed to convert chat messages into waku messages", zap.Error(err))
  1706  				continue
  1707  			}
  1708  
  1709  			wakuPinMessages, err := m.pinMessagesToWakuMessages(pinMessagesToSave, discordCommunity)
  1710  			if err != nil {
  1711  				m.logger.Error("failed to convert pin messages into waku messages", zap.Error(err))
  1712  				continue
  1713  			}
  1714  
  1715  			wakuMessages := append(wakuChatMessages, wakuPinMessages...)
  1716  
  1717  			topics, err := m.archiveManager.GetCommunityChatsTopics(discordCommunity.ID())
  1718  			if err != nil {
  1719  				m.logger.Error("failed to get community chat topics", zap.Error(err))
  1720  				continue
  1721  			}
  1722  
  1723  			startDate := time.Unix(int64(exportData.OldestMessageTimestamp), 0)
  1724  			endDate := time.Now()
  1725  
  1726  			_, err = m.archiveManager.CreateHistoryArchiveTorrentFromMessages(
  1727  				discordCommunity.ID(),
  1728  				wakuMessages,
  1729  				topics,
  1730  				startDate,
  1731  				endDate,
  1732  				messageArchiveInterval,
  1733  				discordCommunity.Encrypted(),
  1734  			)
  1735  			if err != nil {
  1736  				m.logger.Error("failed to create history archive torrent", zap.Error(err))
  1737  				continue
  1738  			}
  1739  
  1740  			if m.archiveManager.IsReady() && communitySettings.HistoryArchiveSupportEnabled {
  1741  
  1742  				err = m.archiveManager.SeedHistoryArchiveTorrent(discordCommunity.ID())
  1743  				if err != nil {
  1744  					m.logger.Error("failed to seed history archive", zap.Error(err))
  1745  				}
  1746  				go m.archiveManager.StartHistoryArchiveTasksInterval(discordCommunity, messageArchiveInterval)
  1747  			}
  1748  		}
  1749  
  1750  		err = m.publishOrg(discordCommunity, false)
  1751  		if err != nil {
  1752  			m.cleanUpImport(communityID)
  1753  			importProgress.AddTaskError(discord.InitCommunityTask, discord.Error(err.Error()))
  1754  			importProgress.Stop()
  1755  			progressUpdates <- importProgress
  1756  			return
  1757  		}
  1758  
  1759  		if m.DiscordImportMarkedAsCancelled(communityID) {
  1760  			importProgress.StopTask(discord.InitCommunityTask)
  1761  			progressUpdates <- importProgress
  1762  			cancel <- communityID
  1763  			return
  1764  		}
  1765  
  1766  		// Chats need to be saved after the community has been published,
  1767  		// hence we make this part of the `InitCommunityTask`
  1768  		err = m.saveChats(chatsToSave)
  1769  		if err != nil {
  1770  			m.cleanUpImport(communityID)
  1771  			importProgress.AddTaskError(discord.InitCommunityTask, discord.Error(err.Error()))
  1772  			importProgress.Stop()
  1773  			progressUpdates <- importProgress
  1774  			return
  1775  		}
  1776  
  1777  		importProgress.UpdateTaskProgress(discord.InitCommunityTask, 0.15)
  1778  		progressUpdates <- importProgress
  1779  
  1780  		if m.DiscordImportMarkedAsCancelled(communityID) {
  1781  			importProgress.StopTask(discord.InitCommunityTask)
  1782  			progressUpdates <- importProgress
  1783  			cancel <- communityID
  1784  			return
  1785  		}
  1786  
  1787  		// Init the community filter so we can receive messages on the community
  1788  		_, err = m.InitCommunityFilters([]transport.CommunityFilterToInitialize{{
  1789  			Shard:   discordCommunity.Shard(),
  1790  			PrivKey: discordCommunity.PrivateKey(),
  1791  		}})
  1792  		if err != nil {
  1793  			m.cleanUpImport(communityID)
  1794  			importProgress.AddTaskError(discord.InitCommunityTask, discord.Error(err.Error()))
  1795  			importProgress.StopTask(discord.InitCommunityTask)
  1796  			progressUpdates <- importProgress
  1797  			return
  1798  		}
  1799  		importProgress.UpdateTaskProgress(discord.InitCommunityTask, 0.25)
  1800  		progressUpdates <- importProgress
  1801  
  1802  		if m.DiscordImportMarkedAsCancelled(communityID) {
  1803  			importProgress.StopTask(discord.InitCommunityTask)
  1804  			progressUpdates <- importProgress
  1805  			cancel <- communityID
  1806  			return
  1807  		}
  1808  
  1809  		_, err = m.transport.InitPublicFilters(m.DefaultFilters(discordCommunity))
  1810  		if err != nil {
  1811  			m.cleanUpImport(communityID)
  1812  			importProgress.AddTaskError(discord.InitCommunityTask, discord.Error(err.Error()))
  1813  			importProgress.StopTask(discord.InitCommunityTask)
  1814  			progressUpdates <- importProgress
  1815  			return
  1816  		}
  1817  
  1818  		importProgress.UpdateTaskProgress(discord.InitCommunityTask, 0.5)
  1819  		progressUpdates <- importProgress
  1820  
  1821  		if m.DiscordImportMarkedAsCancelled(communityID) {
  1822  			importProgress.StopTask(discord.InitCommunityTask)
  1823  			progressUpdates <- importProgress
  1824  			cancel <- communityID
  1825  			return
  1826  		}
  1827  
  1828  		filters := m.transport.Filters()
  1829  		_, err = m.scheduleSyncFilters(filters)
  1830  		if err != nil {
  1831  			m.cleanUpImport(communityID)
  1832  			importProgress.AddTaskError(discord.InitCommunityTask, discord.Error(err.Error()))
  1833  			importProgress.StopTask(discord.InitCommunityTask)
  1834  			progressUpdates <- importProgress
  1835  			return
  1836  		}
  1837  		importProgress.UpdateTaskProgress(discord.InitCommunityTask, 0.75)
  1838  		progressUpdates <- importProgress
  1839  
  1840  		if m.DiscordImportMarkedAsCancelled(communityID) {
  1841  			importProgress.StopTask(discord.InitCommunityTask)
  1842  			progressUpdates <- importProgress
  1843  			cancel <- communityID
  1844  			return
  1845  		}
  1846  
  1847  		err = m.reregisterForPushNotifications()
  1848  		if err != nil {
  1849  			m.cleanUpImport(communityID)
  1850  			importProgress.AddTaskError(discord.InitCommunityTask, discord.Error(err.Error()))
  1851  			importProgress.StopTask(discord.InitCommunityTask)
  1852  			progressUpdates <- importProgress
  1853  			return
  1854  		}
  1855  		importProgress.UpdateTaskProgress(discord.InitCommunityTask, 1)
  1856  		progressUpdates <- importProgress
  1857  
  1858  		if m.DiscordImportMarkedAsCancelled(communityID) {
  1859  			importProgress.StopTask(discord.InitCommunityTask)
  1860  			progressUpdates <- importProgress
  1861  			cancel <- communityID
  1862  			return
  1863  		}
  1864  
  1865  		m.config.messengerSignalsHandler.DiscordCommunityImportFinished(communityID)
  1866  		close(done)
  1867  	}()
  1868  }