feat: cache curated communities in db

closes: status-im/status-desktop#12277
This commit is contained in:
Patryk Osmaczko 2023-11-14 22:06:33 +01:00 committed by osmaczko
parent 5b51c32a4a
commit 2c55b9c676
6 changed files with 236 additions and 121 deletions

View File

@ -637,37 +637,45 @@ type CommunityShard struct {
Shard *common.Shard `json:"shard"`
}
type KnownCommunitiesResponse struct {
ContractCommunities []string `json:"contractCommunities"` // TODO: use CommunityShard
ContractFeaturedCommunities []string `json:"contractFeaturedCommunities"` // TODO: use CommunityShard
Descriptions map[string]*Community `json:"communities"`
UnknownCommunities []string `json:"unknownCommunities"` // TODO: use CommunityShard
type CuratedCommunities struct {
ContractCommunities []string
ContractFeaturedCommunities []string
}
func (m *Manager) GetStoredDescriptionForCommunities(communityIDs []types.HexBytes) (response *KnownCommunitiesResponse, err error) {
response = &KnownCommunitiesResponse{
type KnownCommunitiesResponse struct {
ContractCommunities []string `json:"contractCommunities"`
ContractFeaturedCommunities []string `json:"contractFeaturedCommunities"`
Descriptions map[string]*Community `json:"communities"`
UnknownCommunities []string `json:"unknownCommunities"`
}
func (m *Manager) GetStoredDescriptionForCommunities(communityIDs []string) (*KnownCommunitiesResponse, error) {
response := &KnownCommunitiesResponse{
Descriptions: make(map[string]*Community),
}
for i := range communityIDs {
communityID := communityIDs[i].String()
var community *Community
community, err = m.GetByID(communityIDs[i])
communityID := communityIDs[i]
communityIDBytes, err := types.DecodeHex(communityID)
if err != nil {
return
return nil, err
}
response.ContractCommunities = append(response.ContractCommunities, communityID)
community, err := m.GetByID(types.HexBytes(communityIDBytes))
if err != nil {
return nil, err
}
if community != nil {
response.Descriptions[community.IDString()] = community
} else {
response.UnknownCommunities = append(response.UnknownCommunities, communityID)
}
response.ContractCommunities = append(response.ContractCommunities, communityID)
}
return
return response, nil
}
func (m *Manager) Joined() ([]*Community, error) {
@ -5107,3 +5115,11 @@ func (m *Manager) SafeGetSignerPubKey(chainID uint64, communityID string) (strin
return m.ownerVerifier.SafeGetSignerPubKey(ctx, chainID, communityID)
}
func (m *Manager) GetCuratedCommunities() (*CuratedCommunities, error) {
return m.persistence.GetCuratedCommunities()
}
func (m *Manager) SetCuratedCommunities(communities *CuratedCommunities) error {
return m.persistence.SetCuratedCommunities(communities)
}

View File

@ -1400,7 +1400,7 @@ func decodeEventsData(eventsBytes []byte, eventsDescriptionBytes []byte) (*Event
func (p *Persistence) GetCommunityRequestsToJoinWithRevealedAddresses(communityID []byte) ([]*RequestToJoin, error) {
requests := []*RequestToJoin{}
rows, err := p.db.Query(`
SELECT r.id, r.public_key, r.clock, r.ens_name, r.chat_id, r.state, r.community_id,
SELECT r.id, r.public_key, r.clock, r.ens_name, r.chat_id, r.state, r.community_id,
a.address, a.chain_ids, a.is_airdrop_address, a.signature
FROM communities_requests_to_join r
LEFT JOIN communities_requests_to_join_revealed_addresses a ON r.id = a.request_id
@ -1604,7 +1604,7 @@ func (p *Persistence) SaveRequestsToJoin(requests []*RequestToJoin) (err error)
}
}()
stmt, err := tx.Prepare(`INSERT OR REPLACE INTO communities_requests_to_join(id,public_key,clock,ens_name,chat_id,community_id,state)
stmt, err := tx.Prepare(`INSERT OR REPLACE INTO communities_requests_to_join(id,public_key,clock,ens_name,chat_id,community_id,state)
VALUES (?, ?, ?, ?, ?, ?, ?)`)
if err != nil {
return err
@ -1632,3 +1632,70 @@ func (p *Persistence) SaveRequestsToJoin(requests []*RequestToJoin) (err error)
err = tx.Commit()
return err
}
func (p *Persistence) GetCuratedCommunities() (*CuratedCommunities, error) {
rows, err := p.db.Query("SELECT community_id, featured FROM curated_communities")
if err != nil {
return nil, err
}
defer rows.Close()
result := &CuratedCommunities{
ContractCommunities: []string{},
ContractFeaturedCommunities: []string{},
}
for rows.Next() {
var communityID string
var featured bool
if err := rows.Scan(&communityID, &featured); err != nil {
return nil, err
}
result.ContractCommunities = append(result.ContractCommunities, communityID)
if featured {
result.ContractFeaturedCommunities = append(result.ContractFeaturedCommunities, communityID)
}
}
return result, nil
}
func (p *Persistence) SetCuratedCommunities(communities *CuratedCommunities) error {
tx, err := p.db.BeginTx(context.Background(), &sql.TxOptions{})
if err != nil {
return err
}
defer func() {
if err == nil {
err = tx.Commit()
return
}
// don't shadow original error
_ = tx.Rollback()
}()
// Clear the existing communities
if _, err = tx.Exec("DELETE FROM curated_communities"); err != nil {
return err
}
stmt, err := tx.Prepare("INSERT INTO curated_communities (community_id, featured) VALUES (?, ?)")
if err != nil {
return err
}
defer stmt.Close()
featuredMap := make(map[string]bool)
for _, community := range communities.ContractFeaturedCommunities {
featuredMap[community] = true
}
for _, community := range communities.ContractCommunities {
_, err := stmt.Exec(community, featuredMap[community])
if err != nil {
return err
}
}
return nil
}

View File

@ -4,6 +4,7 @@ import (
"crypto/ecdsa"
"database/sql"
"math/big"
"reflect"
"testing"
"time"
@ -634,3 +635,34 @@ func (s *PersistenceSuite) TestGetCommunityRequestsToJoinWithRevealedAddresses()
s.Require().Len(rtjResult, 4)
s.Require().Len(rtjResult[3].RevealedAccounts, 0)
}
func (s *PersistenceSuite) TestCuratedCommunities() {
communities, err := s.db.GetCuratedCommunities()
s.Require().NoError(err)
s.Require().Empty(communities.ContractCommunities)
s.Require().Empty(communities.ContractFeaturedCommunities)
setCommunities := &CuratedCommunities{
ContractCommunities: []string{"x", "d"},
ContractFeaturedCommunities: []string{"x"},
}
err = s.db.SetCuratedCommunities(setCommunities)
s.Require().NoError(err)
communities, err = s.db.GetCuratedCommunities()
s.Require().NoError(err)
s.Require().True(reflect.DeepEqual(communities, setCommunities))
setCommunities = &CuratedCommunities{
ContractCommunities: []string{"p", "a", "t", "r", "y", "k"},
ContractFeaturedCommunities: []string{"p", "k"},
}
err = s.db.SetCuratedCommunities(setCommunities)
s.Require().NoError(err)
communities, err = s.db.GetCuratedCommunities()
s.Require().NoError(err)
s.Require().True(reflect.DeepEqual(communities, setCommunities))
}

View File

@ -4,7 +4,6 @@ import (
"context"
"errors"
"reflect"
"sync"
"time"
"go.uber.org/zap"
@ -15,66 +14,56 @@ import (
)
const (
fetchError int = 0
fetchSuccess int = 1
fetchHasUnknowns int = 2
curatedCommunitiesUpdateInterval = 2 * time.Minute
)
// Regularly gets list of curated communities and signals them to client
func (m *Messenger) startCuratedCommunitiesUpdateLoop() {
logger := m.logger.Named("startCuratedCommunitiesUpdateLoop")
type curatedCommunities struct {
ContractCommunities []string
ContractFeaturedCommunities []string
UnknownCommunities []string
}
go func() {
// Initialize interval to 0 for immediate execution
var interval time.Duration = 0
var fetchResultsHistory = make([]int, 0)
var mu = sync.RWMutex{}
var c = curatedCommunities{}
cache, err := m.communitiesManager.GetCuratedCommunities()
if err != nil {
logger.Error("failed to start curated communities loop", zap.Error(err))
return
}
for {
response, err := m.CuratedCommunities()
if err != nil {
fetchResultsHistory = append(fetchResultsHistory, fetchError)
} else {
mu.Lock()
// Check if it's the same values we had
if !reflect.DeepEqual(c.ContractCommunities, response.ContractCommunities) ||
!reflect.DeepEqual(c.ContractFeaturedCommunities, response.ContractFeaturedCommunities) ||
!reflect.DeepEqual(c.UnknownCommunities, response.UnknownCommunities) {
// One of the communities is different, send the updated response
m.config.messengerSignalsHandler.SendCuratedCommunitiesUpdate(response)
// Update the values
c.ContractCommunities = response.ContractCommunities
c.ContractFeaturedCommunities = response.ContractFeaturedCommunities
c.UnknownCommunities = response.UnknownCommunities
}
mu.Unlock()
if len(response.UnknownCommunities) == 0 {
fetchResultsHistory = append(fetchResultsHistory, fetchSuccess)
} else {
fetchResultsHistory = append(fetchResultsHistory, fetchHasUnknowns)
}
}
//keep only 2 last fetch results
if len(fetchResultsHistory) > 2 {
fetchResultsHistory = fetchResultsHistory[1:]
}
timeTillNextUpdate := calcTimeTillNextUpdate(fetchResultsHistory)
logger.Debug("Next curated communities update will happen in", zap.Duration("timeTillNextUpdate", timeTillNextUpdate))
select {
case <-time.After(timeTillNextUpdate):
case <-time.After(interval):
// Immediate execution on first run, then set to regular interval
interval = curatedCommunitiesUpdateInterval
curatedCommunities, err := m.getCuratedCommunitiesFromContract()
if err != nil {
logger.Error("failed to get curated communities from contract", zap.Error(err))
continue
}
if reflect.DeepEqual(cache.ContractCommunities, curatedCommunities.ContractCommunities) &&
reflect.DeepEqual(cache.ContractFeaturedCommunities, curatedCommunities.ContractFeaturedCommunities) {
// nothing changed
continue
}
err = m.communitiesManager.SetCuratedCommunities(curatedCommunities)
if err == nil {
cache = curatedCommunities
} else {
logger.Error("failed to save curated communities", zap.Error(err))
}
response, err := m.fetchCuratedCommunities(curatedCommunities)
if err != nil {
logger.Error("failed to fetch curated communities", zap.Error(err))
continue
}
m.config.messengerSignalsHandler.SendCuratedCommunitiesUpdate(response)
case <-m.quit:
return
}
@ -82,39 +71,7 @@ func (m *Messenger) startCuratedCommunitiesUpdateLoop() {
}()
}
func calcTimeTillNextUpdate(fetchResultsHistory []int) time.Duration {
// TODO lower this back again once the real curated community contract is up
// The current contract contains communities that are no longer accessible on waku
const shortTimeout = 30 * time.Second
const averageTimeout = 60 * time.Second
const longTimeout = 300 * time.Second
twoConsecutiveErrors := (len(fetchResultsHistory) == 2 &&
fetchResultsHistory[0] == fetchError &&
fetchResultsHistory[1] == fetchError)
twoConsecutiveHasUnknowns := (len(fetchResultsHistory) == 2 &&
fetchResultsHistory[0] == fetchHasUnknowns &&
fetchResultsHistory[1] == fetchHasUnknowns)
var timeTillNextUpdate time.Duration
if twoConsecutiveErrors || twoConsecutiveHasUnknowns {
timeTillNextUpdate = longTimeout
} else {
switch fetchResultsHistory[len(fetchResultsHistory)-1] {
case fetchError:
timeTillNextUpdate = shortTimeout
case fetchSuccess:
timeTillNextUpdate = longTimeout
case fetchHasUnknowns:
timeTillNextUpdate = averageTimeout
}
}
return timeTillNextUpdate
}
func (m *Messenger) CuratedCommunities() (*communities.KnownCommunitiesResponse, error) {
func (m *Messenger) getCuratedCommunitiesFromContract() (*communities.CuratedCommunities, error) {
if m.contractMaker == nil {
m.logger.Warn("contract maker not initialized")
return nil, errors.New("contract maker not initialized")
@ -137,28 +94,36 @@ func (m *Messenger) CuratedCommunities() (*communities.KnownCommunitiesResponse,
callOpts := &bind.CallOpts{Context: context.Background(), Pending: false}
curatedCommunities, err := directory.GetCommunities(callOpts)
contractCommunities, err := directory.GetCommunities(callOpts)
if err != nil {
return nil, err
}
var communityIDs []types.HexBytes
for _, c := range curatedCommunities {
communityIDs = append(communityIDs, c)
var contractCommunityIDs []string
for _, c := range contractCommunities {
contractCommunityIDs = append(contractCommunityIDs, types.HexBytes(c).String())
}
response, err := m.communitiesManager.GetStoredDescriptionForCommunities(communityIDs)
featuredContractCommunities, err := directory.GetFeaturedCommunities(callOpts)
if err != nil {
return nil, err
}
var contractFeaturedCommunityIDs []string
for _, c := range featuredContractCommunities {
contractFeaturedCommunityIDs = append(contractFeaturedCommunityIDs, types.HexBytes(c).String())
}
featuredCommunities, err := directory.GetFeaturedCommunities(callOpts)
return &communities.CuratedCommunities{
ContractCommunities: contractCommunityIDs,
ContractFeaturedCommunities: contractFeaturedCommunityIDs,
}, nil
}
func (m *Messenger) fetchCuratedCommunities(curatedCommunities *communities.CuratedCommunities) (*communities.KnownCommunitiesResponse, error) {
response, err := m.communitiesManager.GetStoredDescriptionForCommunities(curatedCommunities.ContractCommunities)
if err != nil {
return nil, err
}
for _, c := range featuredCommunities {
response.ContractFeaturedCommunities = append(response.ContractFeaturedCommunities, types.HexBytes(c).String())
}
response.ContractFeaturedCommunities = curatedCommunities.ContractFeaturedCommunities
// TODO: use mechanism to obtain shard from community ID (https://github.com/status-im/status-desktop/issues/12585)
var unknownCommunities []communities.CommunityShard
@ -172,3 +137,11 @@ func (m *Messenger) CuratedCommunities() (*communities.KnownCommunitiesResponse,
return response, nil
}
func (m *Messenger) CuratedCommunities() (*communities.KnownCommunitiesResponse, error) {
curatedCommunities, err := m.communitiesManager.GetCuratedCommunities()
if err != nil {
return nil, err
}
return m.fetchCuratedCommunities(curatedCommunities)
}

View File

@ -112,6 +112,7 @@
// 1699041816_profile_showcase_contacts.up.sql (2.206kB)
// 1699554099_message_segments.up.sql (426B)
// 1700044186_message_segments_timestamp.up.sql (322B)
// 1700044187_curated_communities.up.sql (131B)
// README.md (554B)
// doc.go (850B)
@ -2276,7 +2277,7 @@ func _1697699419_community_control_node_syncUpSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "1697699419_community_control_node_sync.up.sql", size: 435, mode: os.FileMode(0644), modTime: time.Unix(1699030398, 0)}
info := bindataFileInfo{name: "1697699419_community_control_node_sync.up.sql", size: 435, mode: os.FileMode(0644), modTime: time.Unix(1700066424, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x11, 0xd6, 0x63, 0x10, 0x1b, 0x16, 0x35, 0x57, 0xf1, 0x4a, 0x4, 0x51, 0xe0, 0x1, 0xe1, 0xfc, 0x12, 0x3a, 0x10, 0x4f, 0xb1, 0x96, 0x53, 0x2, 0xf5, 0x66, 0x7b, 0xe0, 0x8a, 0xdf, 0x78, 0x53}}
return a, nil
}
@ -2296,7 +2297,7 @@ func _1698137561_add_profile_showcase_tablesUpSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "1698137561_add_profile_showcase_tables.up.sql", size: 440, mode: os.FileMode(0644), modTime: time.Unix(1699030398, 0)}
info := bindataFileInfo{name: "1698137561_add_profile_showcase_tables.up.sql", size: 440, mode: os.FileMode(0644), modTime: time.Unix(1700066424, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x7c, 0xef, 0x89, 0x68, 0x42, 0xbf, 0xff, 0xb9, 0x8f, 0x8f, 0x19, 0x91, 0xd2, 0x6a, 0x85, 0xda, 0x2c, 0x63, 0x5f, 0x3c, 0x84, 0x4, 0x93, 0x16, 0x10, 0xf0, 0xe0, 0xd9, 0x9b, 0xbe, 0x8d, 0x62}}
return a, nil
}
@ -2316,7 +2317,7 @@ func _1698137562_fix_encryption_key_idUpSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "1698137562_fix_encryption_key_id.up.sql", size: 758, mode: os.FileMode(0644), modTime: time.Unix(1699030398, 0)}
info := bindataFileInfo{name: "1698137562_fix_encryption_key_id.up.sql", size: 758, mode: os.FileMode(0644), modTime: time.Unix(1700066424, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xe5, 0x61, 0x1b, 0x6a, 0xb1, 0x44, 0x8d, 0x47, 0xde, 0x55, 0x45, 0x77, 0x8e, 0x4f, 0xb, 0x6a, 0x7f, 0x83, 0x56, 0x9c, 0x80, 0xc0, 0xae, 0xda, 0xd8, 0xaf, 0x7e, 0x2b, 0xb4, 0x5e, 0xc3, 0x63}}
return a, nil
}
@ -2336,7 +2337,7 @@ func _1698414646_add_paddingUpSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "1698414646_add_padding.up.sql", size: 69, mode: os.FileMode(0644), modTime: time.Unix(1699030398, 0)}
info := bindataFileInfo{name: "1698414646_add_padding.up.sql", size: 69, mode: os.FileMode(0644), modTime: time.Unix(1700066424, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xbf, 0x48, 0x8e, 0x18, 0x1b, 0x81, 0x78, 0xab, 0x42, 0xcb, 0x11, 0xf5, 0xe, 0x44, 0xd4, 0x35, 0x33, 0x4e, 0x8, 0x6f, 0x14, 0x90, 0xe6, 0x2b, 0x59, 0xee, 0x87, 0xb, 0x96, 0x62, 0x3, 0x45}}
return a, nil
}
@ -2356,7 +2357,7 @@ func _1698746210_add_signature_to_revealed_addressesUpSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "1698746210_add_signature_to_revealed_addresses.up.sql", size: 87, mode: os.FileMode(0644), modTime: time.Unix(1699030398, 0)}
info := bindataFileInfo{name: "1698746210_add_signature_to_revealed_addresses.up.sql", size: 87, mode: os.FileMode(0644), modTime: time.Unix(1700066424, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x8f, 0x64, 0xef, 0xe7, 0x5d, 0x82, 0x3e, 0x7d, 0x5a, 0x34, 0xd2, 0xa, 0x5c, 0x48, 0xef, 0x40, 0xb4, 0x7d, 0x78, 0xc8, 0x11, 0xbc, 0xf3, 0xc5, 0x1d, 0xd5, 0xe9, 0x39, 0xd9, 0xfa, 0xc8, 0x27}}
return a, nil
}
@ -2376,7 +2377,7 @@ func _1699041816_profile_showcase_contactsUpSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "1699041816_profile_showcase_contacts.up.sql", size: 2206, mode: os.FileMode(0644), modTime: time.Unix(1699887700, 0)}
info := bindataFileInfo{name: "1699041816_profile_showcase_contacts.up.sql", size: 2206, mode: os.FileMode(0644), modTime: time.Unix(1700066424, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xd5, 0x7b, 0x55, 0xda, 0x93, 0x4a, 0x92, 0xf8, 0x45, 0xb2, 0x9f, 0x32, 0xf4, 0x37, 0xc, 0x5f, 0x62, 0xba, 0x33, 0xe2, 0x5c, 0x91, 0x1c, 0xc, 0x7, 0x9, 0xc2, 0x27, 0x5, 0x90, 0x94, 0xf3}}
return a, nil
}
@ -2396,7 +2397,7 @@ func _1699554099_message_segmentsUpSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "1699554099_message_segments.up.sql", size: 426, mode: os.FileMode(0644), modTime: time.Unix(1699976109, 0)}
info := bindataFileInfo{name: "1699554099_message_segments.up.sql", size: 426, mode: os.FileMode(0644), modTime: time.Unix(1700066424, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x73, 0xca, 0xd, 0xfa, 0xfa, 0x17, 0xef, 0x7e, 0x24, 0xf9, 0x28, 0xbd, 0x39, 0x75, 0xff, 0x34, 0x31, 0x27, 0x58, 0x3c, 0x17, 0x77, 0xfd, 0xc2, 0x66, 0x47, 0x63, 0x58, 0x3e, 0xb3, 0x88, 0x1a}}
return a, nil
}
@ -2416,11 +2417,31 @@ func _1700044186_message_segments_timestampUpSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "1700044186_message_segments_timestamp.up.sql", size: 322, mode: os.FileMode(0644), modTime: time.Unix(1700049804, 0)}
info := bindataFileInfo{name: "1700044186_message_segments_timestamp.up.sql", size: 322, mode: os.FileMode(0644), modTime: time.Unix(1700066437, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x3e, 0x4e, 0x7, 0x86, 0x71, 0xc8, 0x1f, 0x2f, 0xf4, 0xbc, 0xc5, 0xc4, 0x37, 0x56, 0xa1, 0x47, 0xd9, 0xc9, 0xfd, 0xdf, 0x9a, 0x48, 0x1d, 0xfd, 0xb4, 0xeb, 0xb6, 0xb1, 0xc2, 0x73, 0x11, 0x19}}
return a, nil
}
var __1700044187_curated_communitiesUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x34\xcc\xb1\x0a\xc2\x30\x10\x87\xf1\xbd\x4f\xf1\x1f\x15\x7c\x03\xa7\xab\x5e\x21\x78\x26\x92\x5c\xa1\x9d\x4a\x69\x22\x64\xa8\x42\x4d\x06\xdf\x5e\x28\xb8\x7e\x7c\xfc\x2e\x9e\x49\x19\x4a\xad\x30\x4c\x07\xeb\x14\x3c\x98\xa0\x01\x4b\xdd\xe6\x92\xe2\xb4\xbc\xd7\xb5\xbe\x72\xc9\xe9\x83\x43\x03\x00\xff\xf2\x9d\x72\x84\xf2\xa0\x78\x78\x73\x27\x3f\xe2\xc6\xe3\x69\x5f\x9e\x69\x2e\x75\x4b\x11\xad\x73\xc2\x64\x77\xd8\xf6\x22\xb8\x72\x47\xbd\x28\x3a\x92\xc0\xcd\xf1\xdc\xfc\x02\x00\x00\xff\xff\xb5\x80\x91\xfe\x83\x00\x00\x00")
func _1700044187_curated_communitiesUpSqlBytes() ([]byte, error) {
return bindataRead(
__1700044187_curated_communitiesUpSql,
"1700044187_curated_communities.up.sql",
)
}
func _1700044187_curated_communitiesUpSql() (*asset, error) {
bytes, err := _1700044187_curated_communitiesUpSqlBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "1700044187_curated_communities.up.sql", size: 131, mode: os.FileMode(0644), modTime: time.Unix(1700066437, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xed, 0xf1, 0xf1, 0x57, 0xb5, 0x83, 0xad, 0x9d, 0x9b, 0xf, 0x49, 0xe, 0x3d, 0xa5, 0xf6, 0xf5, 0x9c, 0x7f, 0xb3, 0xf7, 0x22, 0x43, 0x8a, 0xa0, 0x49, 0xfa, 0xcc, 0x9b, 0xea, 0xac, 0xc0, 0xb9}}
return a, nil
}
var _readmeMd = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x54\x91\xc1\xce\xd3\x30\x10\x84\xef\x7e\x8a\x91\x7a\x01\xa9\x2a\x8f\xc0\x0d\x71\x82\x03\x48\x1c\xc9\x36\x9e\x36\x96\x1c\x6f\xf0\xae\x93\xe6\xed\x91\xa3\xc2\xdf\xff\x66\xed\xd8\x33\xdf\x78\x4f\xa7\x13\xbe\xea\x06\x57\x6c\x35\x39\x31\xa7\x7b\x15\x4f\x5a\xec\x73\x08\xbf\x08\x2d\x79\x7f\x4a\x43\x5b\x86\x17\xfd\x8c\x21\xea\x56\x5e\x47\x90\x4a\x14\x75\x48\xde\x64\x37\x2c\x6a\x96\xae\x99\x48\x05\xf6\x27\x77\x13\xad\x08\xae\x8a\x51\xe7\x25\xf3\xf1\xa9\x9f\xf9\x58\x58\x2c\xad\xbc\xe0\x8b\x56\xf0\x21\x5d\xeb\x4c\x95\xb3\xae\x84\x60\xd4\xdc\xe6\x82\x5d\x1b\x36\x6d\x39\x62\x92\xf5\xb8\x11\xdb\x92\xd3\x28\xce\xe0\x13\xe1\x72\xcd\x3c\x63\xd4\x65\x87\xae\xac\xe8\xc3\x28\x2e\x67\x44\x66\x3a\x21\x25\xa2\x72\xac\x14\x67\xbc\x84\x9f\x53\x32\x8c\x52\x70\x25\x56\xd6\xfd\x8d\x05\x37\xad\x30\x9d\x9f\xa6\x86\x0f\xcd\x58\x7f\xcf\x34\x93\x3b\xed\x90\x9f\xa4\x1f\xcf\x30\x85\x4d\x07\x58\xaf\x7f\x25\xc4\x9d\xf3\x72\x64\x84\xd0\x7f\xf9\x9b\x3a\x2d\x84\xef\x85\x48\x66\x8d\xd8\x88\x9b\x8c\x8c\x98\x5b\xf6\x74\x14\x4e\x33\x0d\xc9\xe0\x93\x38\xda\x12\xc5\x69\xbd\xe4\xf0\x2e\x7a\x78\x07\x1c\xfe\x13\x9f\x91\x29\x31\x95\x7b\x7f\x62\x59\x37\xb4\xe5\x5e\x25\xfe\x33\xee\xd5\x53\x71\xd6\xda\x3a\xd8\xcb\xde\x2e\xf8\xa1\x90\x55\x53\x0c\xc7\xaa\x0d\xe9\x76\x14\x29\x1c\x7b\x68\xdd\x2f\xe1\x6f\x00\x00\x00\xff\xff\x3c\x0a\xc2\xfe\x2a\x02\x00\x00")
func readmeMdBytes() ([]byte, error) {
@ -2664,8 +2685,9 @@ var _bindata = map[string]func() (*asset, error){
"1699041816_profile_showcase_contacts.up.sql": _1699041816_profile_showcase_contactsUpSql,
"1699554099_message_segments.up.sql": _1699554099_message_segmentsUpSql,
"1700044186_message_segments_timestamp.up.sql": _1700044186_message_segments_timestampUpSql,
"README.md": readmeMd,
"doc.go": docGo,
"1700044187_curated_communities.up.sql": _1700044187_curated_communitiesUpSql,
"README.md": readmeMd,
"doc.go": docGo,
}
// AssetDebug is true if the assets were built with the debug flag enabled.
@ -2826,8 +2848,9 @@ var _bintree = &bintree{nil, map[string]*bintree{
"1699041816_profile_showcase_contacts.up.sql": {_1699041816_profile_showcase_contactsUpSql, map[string]*bintree{}},
"1699554099_message_segments.up.sql": {_1699554099_message_segmentsUpSql, map[string]*bintree{}},
"1700044186_message_segments_timestamp.up.sql": {_1700044186_message_segments_timestampUpSql, map[string]*bintree{}},
"README.md": {readmeMd, map[string]*bintree{}},
"doc.go": {docGo, map[string]*bintree{}},
"1700044187_curated_communities.up.sql": {_1700044187_curated_communitiesUpSql, map[string]*bintree{}},
"README.md": {readmeMd, map[string]*bintree{}},
"doc.go": {docGo, map[string]*bintree{}},
}}
// RestoreAsset restores an asset under the given directory.

View File

@ -0,0 +1,4 @@
CREATE TABLE IF NOT EXISTS curated_communities (
community_id TEXT PRIMARY KEY,
featured BOOLEAN NOT NULL DEFAULT FALSE
);