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

     1  //go:build !disable_torrent
     2  // +build !disable_torrent
     3  
     4  // Attribution to Pascal Precht, for further context please view the below issues
     5  // - https://github.com/status-im/status-go/issues/2563
     6  // - https://github.com/status-im/status-go/issues/2565
     7  // - https://github.com/status-im/status-go/issues/2567
     8  // - https://github.com/status-im/status-go/issues/2568
     9  
    10  package communities
    11  
    12  import (
    13  	"crypto/ecdsa"
    14  	"errors"
    15  	"fmt"
    16  	"net"
    17  	"os"
    18  	"path"
    19  	"sort"
    20  	"sync"
    21  	"time"
    22  
    23  	"github.com/status-im/status-go/eth-node/types"
    24  	"github.com/status-im/status-go/params"
    25  	"github.com/status-im/status-go/protocol/encryption"
    26  	"github.com/status-im/status-go/protocol/transport"
    27  	"github.com/status-im/status-go/signal"
    28  
    29  	"github.com/anacrolix/torrent"
    30  	"github.com/anacrolix/torrent/metainfo"
    31  	"go.uber.org/zap"
    32  )
    33  
    34  type archiveMDSlice []*archiveMetadata
    35  
    36  type archiveMetadata struct {
    37  	hash string
    38  	from uint64
    39  }
    40  
    41  func (md archiveMDSlice) Len() int {
    42  	return len(md)
    43  }
    44  
    45  func (md archiveMDSlice) Swap(i, j int) {
    46  	md[i], md[j] = md[j], md[i]
    47  }
    48  
    49  func (md archiveMDSlice) Less(i, j int) bool {
    50  	return md[i].from > md[j].from
    51  }
    52  
    53  type EncodedArchiveData struct {
    54  	padding int
    55  	bytes   []byte
    56  }
    57  
    58  type ArchiveManager struct {
    59  	torrentConfig                *params.TorrentConfig
    60  	torrentClient                *torrent.Client
    61  	torrentTasks                 map[string]metainfo.Hash
    62  	historyArchiveDownloadTasks  map[string]*HistoryArchiveDownloadTask
    63  	historyArchiveTasksWaitGroup sync.WaitGroup
    64  	historyArchiveTasks          sync.Map // stores `chan struct{}`
    65  
    66  	logger      *zap.Logger
    67  	persistence *Persistence
    68  	transport   *transport.Transport
    69  	identity    *ecdsa.PrivateKey
    70  	encryptor   *encryption.Protocol
    71  
    72  	*ArchiveFileManager
    73  	publisher Publisher
    74  }
    75  
    76  // NewArchiveManager this function is only built and called when the "disable_torrent" build tag is not set
    77  // In this case this version of NewArchiveManager will return the full Desktop ArchiveManager ensuring that the
    78  // build command will import and build the torrent deps for the Desktop OSes.
    79  // NOTE: It is intentional that this file contains the identical function name as in "manager_archive_nop.go"
    80  func NewArchiveManager(amc *ArchiveManagerConfig) *ArchiveManager {
    81  	return &ArchiveManager{
    82  		torrentConfig:               amc.TorrentConfig,
    83  		torrentTasks:                make(map[string]metainfo.Hash),
    84  		historyArchiveDownloadTasks: make(map[string]*HistoryArchiveDownloadTask),
    85  
    86  		logger:      amc.Logger,
    87  		persistence: amc.Persistence,
    88  		transport:   amc.Transport,
    89  		identity:    amc.Identity,
    90  		encryptor:   amc.Encryptor,
    91  
    92  		publisher:          amc.Publisher,
    93  		ArchiveFileManager: NewArchiveFileManager(amc),
    94  	}
    95  }
    96  
    97  func (m *ArchiveManager) SetOnline(online bool) {
    98  	if online {
    99  		if m.torrentConfig != nil && m.torrentConfig.Enabled && !m.torrentClientStarted() {
   100  			err := m.StartTorrentClient()
   101  			if err != nil {
   102  				m.logger.Error("couldn't start torrent client", zap.Error(err))
   103  			}
   104  		}
   105  	}
   106  }
   107  
   108  func (m *ArchiveManager) SetTorrentConfig(config *params.TorrentConfig) {
   109  	m.torrentConfig = config
   110  	m.ArchiveFileManager.torrentConfig = config
   111  }
   112  
   113  // getTCPandUDPport will return the same port number given if != 0,
   114  // otherwise, it will attempt to find a free random tcp and udp port using
   115  // the same number for both protocols
   116  func (m *ArchiveManager) getTCPandUDPport(portNumber int) (int, error) {
   117  	if portNumber != 0 {
   118  		return portNumber, nil
   119  	}
   120  
   121  	// Find free port
   122  	for i := 0; i < 10; i++ {
   123  		port := func() int {
   124  			tcpAddr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort("localhost", "0"))
   125  			if err != nil {
   126  				m.logger.Warn("unable to resolve tcp addr: %v", zap.Error(err))
   127  				return 0
   128  			}
   129  
   130  			tcpListener, err := net.ListenTCP("tcp", tcpAddr)
   131  			if err != nil {
   132  				m.logger.Warn("unable to listen on addr", zap.Stringer("addr", tcpAddr), zap.Error(err))
   133  				return 0
   134  			}
   135  			defer tcpListener.Close()
   136  
   137  			port := tcpListener.Addr().(*net.TCPAddr).Port
   138  
   139  			udpAddr, err := net.ResolveUDPAddr("udp", net.JoinHostPort("localhost", fmt.Sprintf("%d", port)))
   140  			if err != nil {
   141  				m.logger.Warn("unable to resolve udp addr: %v", zap.Error(err))
   142  				return 0
   143  			}
   144  
   145  			udpListener, err := net.ListenUDP("udp", udpAddr)
   146  			if err != nil {
   147  				m.logger.Warn("unable to listen on addr", zap.Stringer("addr", udpAddr), zap.Error(err))
   148  				return 0
   149  			}
   150  			defer udpListener.Close()
   151  
   152  			return port
   153  		}()
   154  
   155  		if port != 0 {
   156  			return port, nil
   157  		}
   158  	}
   159  
   160  	return 0, fmt.Errorf("no free port found")
   161  }
   162  
   163  func (m *ArchiveManager) StartTorrentClient() error {
   164  	if m.torrentConfig == nil {
   165  		return fmt.Errorf("can't start torrent client: missing torrentConfig")
   166  	}
   167  
   168  	if m.torrentClientStarted() {
   169  		return nil
   170  	}
   171  
   172  	port, err := m.getTCPandUDPport(m.torrentConfig.Port)
   173  	if err != nil {
   174  		return err
   175  	}
   176  
   177  	config := torrent.NewDefaultClientConfig()
   178  	config.SetListenAddr(":" + fmt.Sprint(port))
   179  	config.Seed = true
   180  
   181  	config.DataDir = m.torrentConfig.DataDir
   182  
   183  	if _, err := os.Stat(m.torrentConfig.DataDir); os.IsNotExist(err) {
   184  		err := os.MkdirAll(m.torrentConfig.DataDir, 0700)
   185  		if err != nil {
   186  			return err
   187  		}
   188  	}
   189  
   190  	m.logger.Info("Starting torrent client", zap.Any("port", port))
   191  	// Instantiating the client will make it bootstrap and listen eagerly,
   192  	// so no go routine is needed here
   193  	client, err := torrent.NewClient(config)
   194  	if err != nil {
   195  		return err
   196  	}
   197  	m.torrentClient = client
   198  	return nil
   199  }
   200  
   201  func (m *ArchiveManager) Stop() error {
   202  	if m.torrentClientStarted() {
   203  		m.stopHistoryArchiveTasksIntervals()
   204  		m.logger.Info("Stopping torrent client")
   205  		errs := m.torrentClient.Close()
   206  		if len(errs) > 0 {
   207  			return errors.Join(errs...)
   208  		}
   209  		m.torrentClient = nil
   210  	}
   211  	return nil
   212  }
   213  
   214  func (m *ArchiveManager) torrentClientStarted() bool {
   215  	return m.torrentClient != nil
   216  }
   217  
   218  func (m *ArchiveManager) IsReady() bool {
   219  	// Simply checking for `torrentConfig.Enabled` isn't enough
   220  	// as there's a possibility that the torrent client couldn't
   221  	// be instantiated (for example in case of port conflicts)
   222  	return m.torrentConfig != nil &&
   223  		m.torrentConfig.Enabled &&
   224  		m.torrentClientStarted()
   225  }
   226  
   227  func (m *ArchiveManager) GetCommunityChatsFilters(communityID types.HexBytes) ([]*transport.Filter, error) {
   228  	chatIDs, err := m.persistence.GetCommunityChatIDs(communityID)
   229  	if err != nil {
   230  		return nil, err
   231  	}
   232  
   233  	filters := []*transport.Filter{}
   234  	for _, cid := range chatIDs {
   235  		filters = append(filters, m.transport.FilterByChatID(cid))
   236  	}
   237  	return filters, nil
   238  }
   239  
   240  func (m *ArchiveManager) GetCommunityChatsTopics(communityID types.HexBytes) ([]types.TopicType, error) {
   241  	filters, err := m.GetCommunityChatsFilters(communityID)
   242  	if err != nil {
   243  		return nil, err
   244  	}
   245  
   246  	topics := []types.TopicType{}
   247  	for _, filter := range filters {
   248  		topics = append(topics, filter.ContentTopic)
   249  	}
   250  
   251  	return topics, nil
   252  }
   253  
   254  func (m *ArchiveManager) getOldestWakuMessageTimestamp(topics []types.TopicType) (uint64, error) {
   255  	return m.persistence.GetOldestWakuMessageTimestamp(topics)
   256  }
   257  
   258  func (m *ArchiveManager) getLastMessageArchiveEndDate(communityID types.HexBytes) (uint64, error) {
   259  	return m.persistence.GetLastMessageArchiveEndDate(communityID)
   260  }
   261  
   262  func (m *ArchiveManager) GetHistoryArchivePartitionStartTimestamp(communityID types.HexBytes) (uint64, error) {
   263  	filters, err := m.GetCommunityChatsFilters(communityID)
   264  	if err != nil {
   265  		m.logger.Error("failed to get community chats filters", zap.Error(err))
   266  		return 0, err
   267  	}
   268  
   269  	if len(filters) == 0 {
   270  		// If we don't have chat filters, we likely don't have any chats
   271  		// associated to this community, which means there's nothing more
   272  		// to do here
   273  		return 0, nil
   274  	}
   275  
   276  	topics := []types.TopicType{}
   277  
   278  	for _, filter := range filters {
   279  		topics = append(topics, filter.ContentTopic)
   280  	}
   281  
   282  	lastArchiveEndDateTimestamp, err := m.getLastMessageArchiveEndDate(communityID)
   283  	if err != nil {
   284  		m.logger.Error("failed to get last archive end date", zap.Error(err))
   285  		return 0, err
   286  	}
   287  
   288  	if lastArchiveEndDateTimestamp == 0 {
   289  		// If we don't have a tracked last message archive end date, it
   290  		// means we haven't created an archive before, which means
   291  		// the next thing to look at is the oldest waku message timestamp for
   292  		// this community
   293  		lastArchiveEndDateTimestamp, err = m.getOldestWakuMessageTimestamp(topics)
   294  		if err != nil {
   295  			m.logger.Error("failed to get oldest waku message timestamp", zap.Error(err))
   296  			return 0, err
   297  		}
   298  		if lastArchiveEndDateTimestamp == 0 {
   299  			// This means there's no waku message stored for this community so far
   300  			// (even after requesting possibly missed messages), so no messages exist yet that can be archived
   301  			m.logger.Debug("can't find valid `lastArchiveEndTimestamp`")
   302  			return 0, nil
   303  		}
   304  	}
   305  
   306  	return lastArchiveEndDateTimestamp, nil
   307  }
   308  
   309  func (m *ArchiveManager) CreateAndSeedHistoryArchive(communityID types.HexBytes, topics []types.TopicType, startDate time.Time, endDate time.Time, partition time.Duration, encrypt bool) error {
   310  	m.UnseedHistoryArchiveTorrent(communityID)
   311  	_, err := m.ArchiveFileManager.CreateHistoryArchiveTorrentFromDB(communityID, topics, startDate, endDate, partition, encrypt)
   312  	if err != nil {
   313  		return err
   314  	}
   315  	return m.SeedHistoryArchiveTorrent(communityID)
   316  }
   317  
   318  func (m *ArchiveManager) StartHistoryArchiveTasksInterval(community *Community, interval time.Duration) {
   319  	id := community.IDString()
   320  	if _, exists := m.historyArchiveTasks.Load(id); exists {
   321  		m.logger.Error("history archive tasks interval already in progress", zap.String("id", id))
   322  		return
   323  	}
   324  
   325  	cancel := make(chan struct{})
   326  	m.historyArchiveTasks.Store(id, cancel)
   327  	m.historyArchiveTasksWaitGroup.Add(1)
   328  
   329  	ticker := time.NewTicker(interval)
   330  	defer ticker.Stop()
   331  
   332  	m.logger.Debug("starting history archive tasks interval", zap.String("id", id))
   333  	for {
   334  		select {
   335  		case <-ticker.C:
   336  			m.logger.Debug("starting archive task...", zap.String("id", id))
   337  			lastArchiveEndDateTimestamp, err := m.GetHistoryArchivePartitionStartTimestamp(community.ID())
   338  			if err != nil {
   339  				m.logger.Error("failed to get last archive end date", zap.Error(err))
   340  				continue
   341  			}
   342  
   343  			if lastArchiveEndDateTimestamp == 0 {
   344  				// This means there are no waku messages for this community,
   345  				// so nothing to do here
   346  				m.logger.Debug("couldn't determine archive start date - skipping")
   347  				continue
   348  			}
   349  
   350  			topics, err := m.GetCommunityChatsTopics(community.ID())
   351  			if err != nil {
   352  				m.logger.Error("failed to get community chat topics ", zap.Error(err))
   353  				continue
   354  			}
   355  
   356  			ts := time.Now().Unix()
   357  			to := time.Unix(ts, 0)
   358  			lastArchiveEndDate := time.Unix(int64(lastArchiveEndDateTimestamp), 0)
   359  
   360  			err = m.CreateAndSeedHistoryArchive(community.ID(), topics, lastArchiveEndDate, to, interval, community.Encrypted())
   361  			if err != nil {
   362  				m.logger.Error("failed to create and seed history archive", zap.Error(err))
   363  				continue
   364  			}
   365  		case <-cancel:
   366  			m.UnseedHistoryArchiveTorrent(community.ID())
   367  			m.historyArchiveTasks.Delete(id)
   368  			m.historyArchiveTasksWaitGroup.Done()
   369  			return
   370  		}
   371  	}
   372  }
   373  
   374  func (m *ArchiveManager) stopHistoryArchiveTasksIntervals() {
   375  	m.historyArchiveTasks.Range(func(_, task interface{}) bool {
   376  		close(task.(chan struct{})) // Need to cast to the chan
   377  		return true
   378  	})
   379  	// Stoping archive interval tasks is async, so we need
   380  	// to wait for all of them to be closed before we shutdown
   381  	// the torrent client
   382  	m.historyArchiveTasksWaitGroup.Wait()
   383  }
   384  
   385  func (m *ArchiveManager) StopHistoryArchiveTasksInterval(communityID types.HexBytes) {
   386  	task, exists := m.historyArchiveTasks.Load(communityID.String())
   387  	if exists {
   388  		m.logger.Info("Stopping history archive tasks interval", zap.Any("id", communityID.String()))
   389  		close(task.(chan struct{})) // Need to cast to the chan
   390  	}
   391  }
   392  
   393  func (m *ArchiveManager) SeedHistoryArchiveTorrent(communityID types.HexBytes) error {
   394  	m.UnseedHistoryArchiveTorrent(communityID)
   395  
   396  	id := communityID.String()
   397  	torrentFile := torrentFile(m.torrentConfig.TorrentDir, id)
   398  
   399  	metaInfo, err := metainfo.LoadFromFile(torrentFile)
   400  	if err != nil {
   401  		return err
   402  	}
   403  
   404  	info, err := metaInfo.UnmarshalInfo()
   405  	if err != nil {
   406  		return err
   407  	}
   408  
   409  	hash := metaInfo.HashInfoBytes()
   410  	m.torrentTasks[id] = hash
   411  
   412  	if err != nil {
   413  		return err
   414  	}
   415  
   416  	torrent, err := m.torrentClient.AddTorrent(metaInfo)
   417  	if err != nil {
   418  		return err
   419  	}
   420  
   421  	torrent.DownloadAll()
   422  
   423  	m.publisher.publish(&Subscription{
   424  		HistoryArchivesSeedingSignal: &signal.HistoryArchivesSeedingSignal{
   425  			CommunityID: communityID.String(),
   426  		},
   427  	})
   428  
   429  	magnetLink := metaInfo.Magnet(nil, &info).String()
   430  
   431  	m.logger.Debug("seeding torrent", zap.String("id", id), zap.String("magnetLink", magnetLink))
   432  	return nil
   433  }
   434  
   435  func (m *ArchiveManager) UnseedHistoryArchiveTorrent(communityID types.HexBytes) {
   436  	id := communityID.String()
   437  
   438  	hash, exists := m.torrentTasks[id]
   439  
   440  	if exists {
   441  		torrent, ok := m.torrentClient.Torrent(hash)
   442  		if ok {
   443  			m.logger.Debug("Unseeding and dropping torrent for community: ", zap.Any("id", id))
   444  			torrent.Drop()
   445  			delete(m.torrentTasks, id)
   446  
   447  			m.publisher.publish(&Subscription{
   448  				HistoryArchivesUnseededSignal: &signal.HistoryArchivesUnseededSignal{
   449  					CommunityID: id,
   450  				},
   451  			})
   452  		}
   453  	}
   454  }
   455  
   456  func (m *ArchiveManager) IsSeedingHistoryArchiveTorrent(communityID types.HexBytes) bool {
   457  	id := communityID.String()
   458  	hash := m.torrentTasks[id]
   459  	torrent, ok := m.torrentClient.Torrent(hash)
   460  	return ok && torrent.Seeding()
   461  }
   462  
   463  func (m *ArchiveManager) GetHistoryArchiveDownloadTask(communityID string) *HistoryArchiveDownloadTask {
   464  	return m.historyArchiveDownloadTasks[communityID]
   465  }
   466  
   467  func (m *ArchiveManager) AddHistoryArchiveDownloadTask(communityID string, task *HistoryArchiveDownloadTask) {
   468  	m.historyArchiveDownloadTasks[communityID] = task
   469  }
   470  
   471  func (m *ArchiveManager) DownloadHistoryArchivesByMagnetlink(communityID types.HexBytes, magnetlink string, cancelTask chan struct{}) (*HistoryArchiveDownloadTaskInfo, error) {
   472  
   473  	id := communityID.String()
   474  
   475  	ml, err := metainfo.ParseMagnetUri(magnetlink)
   476  	if err != nil {
   477  		return nil, err
   478  	}
   479  
   480  	m.logger.Debug("adding torrent via magnetlink for community", zap.String("id", id), zap.String("magnetlink", magnetlink))
   481  	torrent, err := m.torrentClient.AddMagnet(magnetlink)
   482  	if err != nil {
   483  		return nil, err
   484  	}
   485  
   486  	downloadTaskInfo := &HistoryArchiveDownloadTaskInfo{
   487  		TotalDownloadedArchivesCount: 0,
   488  		TotalArchivesCount:           0,
   489  		Cancelled:                    false,
   490  	}
   491  
   492  	m.torrentTasks[id] = ml.InfoHash
   493  	timeout := time.After(20 * time.Second)
   494  
   495  	m.logger.Debug("fetching torrent info", zap.String("magnetlink", magnetlink))
   496  	select {
   497  	case <-timeout:
   498  		return nil, ErrTorrentTimedout
   499  	case <-cancelTask:
   500  		m.logger.Debug("cancelled fetching torrent info")
   501  		downloadTaskInfo.Cancelled = true
   502  		return downloadTaskInfo, nil
   503  	case <-torrent.GotInfo():
   504  
   505  		files := torrent.Files()
   506  
   507  		i, ok := findIndexFile(files)
   508  		if !ok {
   509  			// We're dealing with a malformed torrent, so don't do anything
   510  			return nil, errors.New("malformed torrent data")
   511  		}
   512  
   513  		indexFile := files[i]
   514  		indexFile.Download()
   515  
   516  		m.logger.Debug("downloading history archive index")
   517  		ticker := time.NewTicker(100 * time.Millisecond)
   518  		defer ticker.Stop()
   519  
   520  		for {
   521  			select {
   522  			case <-cancelTask:
   523  				m.logger.Debug("cancelled downloading archive index")
   524  				downloadTaskInfo.Cancelled = true
   525  				return downloadTaskInfo, nil
   526  			case <-ticker.C:
   527  				if indexFile.BytesCompleted() == indexFile.Length() {
   528  
   529  					index, err := m.ArchiveFileManager.LoadHistoryArchiveIndexFromFile(m.identity, communityID)
   530  					if err != nil {
   531  						return nil, err
   532  					}
   533  
   534  					existingArchiveIDs, err := m.persistence.GetDownloadedMessageArchiveIDs(communityID)
   535  					if err != nil {
   536  						return nil, err
   537  					}
   538  
   539  					if len(existingArchiveIDs) == len(index.Archives) {
   540  						m.logger.Debug("download cancelled, no new archives")
   541  						return downloadTaskInfo, nil
   542  					}
   543  
   544  					downloadTaskInfo.TotalDownloadedArchivesCount = len(existingArchiveIDs)
   545  					downloadTaskInfo.TotalArchivesCount = len(index.Archives)
   546  
   547  					archiveHashes := make(archiveMDSlice, 0, downloadTaskInfo.TotalArchivesCount)
   548  
   549  					for hash, metadata := range index.Archives {
   550  						archiveHashes = append(archiveHashes, &archiveMetadata{hash: hash, from: metadata.Metadata.From})
   551  					}
   552  
   553  					sort.Sort(sort.Reverse(archiveHashes))
   554  
   555  					m.publisher.publish(&Subscription{
   556  						DownloadingHistoryArchivesStartedSignal: &signal.DownloadingHistoryArchivesStartedSignal{
   557  							CommunityID: communityID.String(),
   558  						},
   559  					})
   560  
   561  					for _, hd := range archiveHashes {
   562  
   563  						hash := hd.hash
   564  						hasArchive := false
   565  
   566  						for _, existingHash := range existingArchiveIDs {
   567  							if existingHash == hash {
   568  								hasArchive = true
   569  								break
   570  							}
   571  						}
   572  						if hasArchive {
   573  							continue
   574  						}
   575  
   576  						metadata := index.Archives[hash]
   577  						startIndex := int(metadata.Offset) / pieceLength
   578  						endIndex := startIndex + int(metadata.Size)/pieceLength
   579  
   580  						downloadMsg := fmt.Sprintf("downloading data for message archive (%d/%d)", downloadTaskInfo.TotalDownloadedArchivesCount+1, downloadTaskInfo.TotalArchivesCount)
   581  						m.logger.Debug(downloadMsg, zap.String("hash", hash))
   582  						m.logger.Debug("pieces (start, end)", zap.Any("startIndex", startIndex), zap.Any("endIndex", endIndex-1))
   583  						torrent.DownloadPieces(startIndex, endIndex)
   584  
   585  						piecesCompleted := make(map[int]bool)
   586  						for i = startIndex; i < endIndex; i++ {
   587  							piecesCompleted[i] = false
   588  						}
   589  
   590  						psc := torrent.SubscribePieceStateChanges()
   591  						downloadTicker := time.NewTicker(1 * time.Second)
   592  						defer downloadTicker.Stop()
   593  
   594  					downloadLoop:
   595  						for {
   596  							select {
   597  							case <-downloadTicker.C:
   598  								done := true
   599  								for i = startIndex; i < endIndex; i++ {
   600  									piecesCompleted[i] = torrent.PieceState(i).Complete
   601  									if !piecesCompleted[i] {
   602  										done = false
   603  									}
   604  								}
   605  								if done {
   606  									psc.Close()
   607  									break downloadLoop
   608  								}
   609  							case <-cancelTask:
   610  								m.logger.Debug("downloading archive data interrupted")
   611  								downloadTaskInfo.Cancelled = true
   612  								return downloadTaskInfo, nil
   613  							}
   614  						}
   615  						downloadTaskInfo.TotalDownloadedArchivesCount++
   616  						err = m.persistence.SaveMessageArchiveID(communityID, hash)
   617  						if err != nil {
   618  							m.logger.Error("couldn't save message archive ID", zap.Error(err))
   619  							continue
   620  						}
   621  						m.publisher.publish(&Subscription{
   622  							HistoryArchiveDownloadedSignal: &signal.HistoryArchiveDownloadedSignal{
   623  								CommunityID: communityID.String(),
   624  								From:        int(metadata.Metadata.From),
   625  								To:          int(metadata.Metadata.To),
   626  							},
   627  						})
   628  					}
   629  					m.publisher.publish(&Subscription{
   630  						HistoryArchivesSeedingSignal: &signal.HistoryArchivesSeedingSignal{
   631  							CommunityID: communityID.String(),
   632  						},
   633  					})
   634  					m.logger.Debug("finished downloading archives")
   635  					return downloadTaskInfo, nil
   636  				}
   637  			}
   638  		}
   639  	}
   640  }
   641  
   642  func (m *ArchiveManager) TorrentFileExists(communityID string) bool {
   643  	_, err := os.Stat(torrentFile(m.torrentConfig.TorrentDir, communityID))
   644  	return err == nil
   645  }
   646  
   647  func topicsAsByteArrays(topics []types.TopicType) [][]byte {
   648  	var topicsAsByteArrays [][]byte
   649  	for _, t := range topics {
   650  		topic := types.TopicTypeToByteArray(t)
   651  		topicsAsByteArrays = append(topicsAsByteArrays, topic)
   652  	}
   653  	return topicsAsByteArrays
   654  }
   655  
   656  func findIndexFile(files []*torrent.File) (index int, ok bool) {
   657  	for i, f := range files {
   658  		if f.DisplayPath() == "index" {
   659  			return i, true
   660  		}
   661  	}
   662  	return 0, false
   663  }
   664  
   665  func torrentFile(torrentDir, communityID string) string {
   666  	return path.Join(torrentDir, communityID+".torrent")
   667  }