chore: populate Community with PubsubTopicPrivateKey
part of: status-im/status-desktop#12408
This commit is contained in:
parent
a0bd3c9a94
commit
25f25e9853
|
@ -12,6 +12,8 @@ import (
|
||||||
"github.com/golang/protobuf/proto"
|
"github.com/golang/protobuf/proto"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
|
|
||||||
"github.com/status-im/status-go/eth-node/crypto"
|
"github.com/status-im/status-go/eth-node/crypto"
|
||||||
"github.com/status-im/status-go/eth-node/types"
|
"github.com/status-im/status-go/eth-node/types"
|
||||||
"github.com/status-im/status-go/images"
|
"github.com/status-im/status-go/images"
|
||||||
|
@ -45,6 +47,7 @@ type Config struct {
|
||||||
SyncedAt uint64
|
SyncedAt uint64
|
||||||
EventsData *EventsData
|
EventsData *EventsData
|
||||||
Shard *common.Shard
|
Shard *common.Shard
|
||||||
|
PubsubTopicPrivateKey *ecdsa.PrivateKey
|
||||||
}
|
}
|
||||||
|
|
||||||
type EventsData struct {
|
type EventsData struct {
|
||||||
|
@ -142,6 +145,7 @@ func (o *Community) MarshalPublicAPIJSON() ([]byte, error) {
|
||||||
CommunityTokensMetadata []*protobuf.CommunityTokenMetadata `json:"communityTokensMetadata"`
|
CommunityTokensMetadata []*protobuf.CommunityTokenMetadata `json:"communityTokensMetadata"`
|
||||||
ActiveMembersCount uint64 `json:"activeMembersCount"`
|
ActiveMembersCount uint64 `json:"activeMembersCount"`
|
||||||
PubsubTopic string `json:"pubsubTopic"`
|
PubsubTopic string `json:"pubsubTopic"`
|
||||||
|
PubsubTopicKey string `json:"pubsubTopicKey"`
|
||||||
Shard *common.Shard `json:"shard"`
|
Shard *common.Shard `json:"shard"`
|
||||||
}{
|
}{
|
||||||
ID: o.ID(),
|
ID: o.ID(),
|
||||||
|
@ -150,6 +154,7 @@ func (o *Community) MarshalPublicAPIJSON() ([]byte, error) {
|
||||||
Categories: make(map[string]CommunityCategory),
|
Categories: make(map[string]CommunityCategory),
|
||||||
Tags: o.Tags(),
|
Tags: o.Tags(),
|
||||||
PubsubTopic: o.PubsubTopic(),
|
PubsubTopic: o.PubsubTopic(),
|
||||||
|
PubsubTopicKey: o.PubsubTopicKey(),
|
||||||
Shard: o.Shard(),
|
Shard: o.Shard(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -258,6 +263,7 @@ func (o *Community) MarshalJSON() ([]byte, error) {
|
||||||
CommunityTokensMetadata []*protobuf.CommunityTokenMetadata `json:"communityTokensMetadata"`
|
CommunityTokensMetadata []*protobuf.CommunityTokenMetadata `json:"communityTokensMetadata"`
|
||||||
ActiveMembersCount uint64 `json:"activeMembersCount"`
|
ActiveMembersCount uint64 `json:"activeMembersCount"`
|
||||||
PubsubTopic string `json:"pubsubTopic"`
|
PubsubTopic string `json:"pubsubTopic"`
|
||||||
|
PubsubTopicKey string `json:"pubsubTopicKey"`
|
||||||
Shard *common.Shard `json:"shard"`
|
Shard *common.Shard `json:"shard"`
|
||||||
}{
|
}{
|
||||||
ID: o.ID(),
|
ID: o.ID(),
|
||||||
|
@ -279,6 +285,7 @@ func (o *Community) MarshalJSON() ([]byte, error) {
|
||||||
Tags: o.Tags(),
|
Tags: o.Tags(),
|
||||||
Encrypted: o.Encrypted(),
|
Encrypted: o.Encrypted(),
|
||||||
PubsubTopic: o.PubsubTopic(),
|
PubsubTopic: o.PubsubTopic(),
|
||||||
|
PubsubTopicKey: o.PubsubTopicKey(),
|
||||||
Shard: o.Shard(),
|
Shard: o.Shard(),
|
||||||
}
|
}
|
||||||
if o.config.CommunityDescription != nil {
|
if o.config.CommunityDescription != nil {
|
||||||
|
@ -1308,6 +1315,21 @@ func (o *Community) PubsubTopic() string {
|
||||||
return transport.GetPubsubTopic(o.Shard().TransportShard())
|
return transport.GetPubsubTopic(o.Shard().TransportShard())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *Community) PubsubTopicPrivateKey() *ecdsa.PrivateKey {
|
||||||
|
return o.config.PubsubTopicPrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Community) SetPubsubTopicPrivateKey(privKey *ecdsa.PrivateKey) {
|
||||||
|
o.config.PubsubTopicPrivateKey = privKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Community) PubsubTopicKey() string {
|
||||||
|
if o.config.PubsubTopicPrivateKey == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return hexutil.Encode(crypto.FromECDSAPub(&o.config.PubsubTopicPrivateKey.PublicKey))
|
||||||
|
}
|
||||||
|
|
||||||
func (o *Community) DefaultFilters() []transport.FiltersToInitialize {
|
func (o *Community) DefaultFilters() []transport.FiltersToInitialize {
|
||||||
cID := o.IDString()
|
cID := o.IDString()
|
||||||
uncompressedPubKey := common.PubkeyToHex(o.config.ID)[2:]
|
uncompressedPubKey := common.PubkeyToHex(o.config.ID)[2:]
|
||||||
|
|
|
@ -625,7 +625,7 @@ func (m *Manager) All() ([]*Community, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range communities {
|
for _, c := range communities {
|
||||||
err = initializeCommunity(c)
|
err = m.initializeCommunity(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -687,7 +687,7 @@ func (m *Manager) Joined() ([]*Community, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range communities {
|
for _, c := range communities {
|
||||||
err = initializeCommunity(c)
|
err = m.initializeCommunity(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -703,7 +703,7 @@ func (m *Manager) Spectated() ([]*Community, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range communities {
|
for _, c := range communities {
|
||||||
err = initializeCommunity(c)
|
err = m.initializeCommunity(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -719,7 +719,7 @@ func (m *Manager) JoinedAndPendingCommunitiesWithRequests() ([]*Community, error
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range communities {
|
for _, c := range communities {
|
||||||
err = initializeCommunity(c)
|
err = m.initializeCommunity(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -735,7 +735,7 @@ func (m *Manager) DeletedCommunities() ([]*Community, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range communities {
|
for _, c := range communities {
|
||||||
err = initializeCommunity(c)
|
err = m.initializeCommunity(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -754,7 +754,7 @@ func (m *Manager) Controlled() ([]*Community, error) {
|
||||||
|
|
||||||
for _, c := range communities {
|
for _, c := range communities {
|
||||||
if c.IsControlNode() {
|
if c.IsControlNode() {
|
||||||
err = initializeCommunity(c)
|
err = m.initializeCommunity(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -1116,6 +1116,18 @@ func (m *Manager) SetShard(communityID types.HexBytes, shard *common.Shard) (*Co
|
||||||
return community, nil
|
return community, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) UpdatePubsubTopicPrivateKey(community *Community, privKey *ecdsa.PrivateKey) error {
|
||||||
|
community.SetPubsubTopicPrivateKey(privKey)
|
||||||
|
|
||||||
|
if privKey != nil {
|
||||||
|
if err := m.transport.StorePubsubTopicKey(community.PubsubTopic(), privKey); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// EditCommunity takes a description, updates the community with the description,
|
// EditCommunity takes a description, updates the community with the description,
|
||||||
// saves it and returns it
|
// saves it and returns it
|
||||||
func (m *Manager) EditCommunity(request *requests.EditCommunity) (*Community, error) {
|
func (m *Manager) EditCommunity(request *requests.EditCommunity) (*Community, error) {
|
||||||
|
@ -3188,12 +3200,20 @@ func (m *Manager) BanUserFromCommunity(request *requests.BanUserFromCommunity) (
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply events to raw community
|
// Apply events to raw community
|
||||||
func initializeCommunity(community *Community) error {
|
func (m *Manager) initializeCommunity(community *Community) error {
|
||||||
err := community.updateCommunityDescriptionByEvents()
|
err := community.updateCommunityDescriptionByEvents()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if m.transport != nil && m.transport.WakuVersion() == 2 {
|
||||||
|
privKey, err := m.transport.RetrievePubsubTopicKey(community.PubsubTopic())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
community.config.PubsubTopicPrivateKey = privKey
|
||||||
|
}
|
||||||
|
|
||||||
// Workaround for https://github.com/status-im/status-desktop/issues/12188
|
// Workaround for https://github.com/status-im/status-desktop/issues/12188
|
||||||
HydrateChannelsMembers(community.IDString(), community.config.CommunityDescription)
|
HydrateChannelsMembers(community.IDString(), community.config.CommunityDescription)
|
||||||
|
|
||||||
|
@ -3209,7 +3229,7 @@ func (m *Manager) GetByID(id []byte) (*Community, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err = initializeCommunity(community)
|
err = m.initializeCommunity(community)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -2084,7 +2084,12 @@ func (m *Messenger) SetCommunityShard(request *requests.SetCommunityShard) (*Mes
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = m.UpdateCommunityFilters(community, topicPrivKey)
|
err = m.communitiesManager.UpdatePubsubTopicPrivateKey(community, topicPrivKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.UpdateCommunityFilters(community)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -2095,22 +2100,12 @@ func (m *Messenger) SetCommunityShard(request *requests.SetCommunityShard) (*Mes
|
||||||
}
|
}
|
||||||
|
|
||||||
response := &MessengerResponse{}
|
response := &MessengerResponse{}
|
||||||
response.AddProtectedTopic(&ProtectedTopic{
|
response.AddCommunity(community)
|
||||||
CommunityID: community.IDString(),
|
|
||||||
PubsubTopic: community.PubsubTopic(),
|
|
||||||
PublicKey: hexutil.Encode(crypto.FromECDSAPub(&topicPrivKey.PublicKey)),
|
|
||||||
})
|
|
||||||
|
|
||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Messenger) UpdateCommunityFilters(community *communities.Community, privKey *ecdsa.PrivateKey) error {
|
func (m *Messenger) UpdateCommunityFilters(community *communities.Community) error {
|
||||||
if m.transport.WakuVersion() == 2 && privKey != nil {
|
|
||||||
if err := m.transport.StorePubsubTopicKey(community.PubsubTopic(), privKey); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
publicFiltersToInit := make([]transport.FiltersToInitialize, 0, len(community.DefaultFilters())+len(community.Chats()))
|
publicFiltersToInit := make([]transport.FiltersToInitialize, 0, len(community.DefaultFilters())+len(community.Chats()))
|
||||||
|
|
||||||
publicFiltersToInit = append(publicFiltersToInit, community.DefaultFilters()...)
|
publicFiltersToInit = append(publicFiltersToInit, community.DefaultFilters()...)
|
||||||
|
@ -2499,11 +2494,7 @@ func (m *Messenger) SendCommunityShardKey(community *communities.Community, pubk
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
key, err := m.transport.RetrievePubsubTopicKey(community.PubsubTopic())
|
key := community.PubsubTopicPrivateKey()
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if key == nil {
|
if key == nil {
|
||||||
return nil // No community shard key available
|
return nil // No community shard key available
|
||||||
}
|
}
|
||||||
|
@ -3073,11 +3064,14 @@ func (m *Messenger) HandleCommunityShardKey(state *ReceivedMessageState, message
|
||||||
return errors.New("signer can't be nil")
|
return errors.New("signer can't be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !community.IsMemberOwner(signer) {
|
err = m.handleCommunityShardAndFiltersFromProto(community, common.ShardFromProtobuff(message.Shard), message.PrivateKey)
|
||||||
return communities.ErrNotAuthorized
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return m.handleCommunityShardAndFiltersFromProto(community, common.ShardFromProtobuff(message.Shard), message.PrivateKey)
|
state.Response.AddCommunity(community)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Messenger) handleCommunityShardAndFiltersFromProto(community *communities.Community, shard *common.Shard, privateKeyBytes []byte) error {
|
func (m *Messenger) handleCommunityShardAndFiltersFromProto(community *communities.Community, shard *common.Shard, privateKeyBytes []byte) error {
|
||||||
|
@ -3094,7 +3088,12 @@ func (m *Messenger) handleCommunityShardAndFiltersFromProto(community *communiti
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = m.UpdateCommunityFilters(community, privKey)
|
err = m.communitiesManager.UpdatePubsubTopicPrivateKey(community, privKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.UpdateCommunityFilters(community)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,12 +34,6 @@ type ClearedHistory struct {
|
||||||
ClearedAt uint64 `json:"clearedAt"`
|
ClearedAt uint64 `json:"clearedAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProtectedTopic struct {
|
|
||||||
CommunityID string `json:"communityID"`
|
|
||||||
PubsubTopic string `json:"pubsubTopic"`
|
|
||||||
PublicKey string `json:"publicKey"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MessengerResponse struct {
|
type MessengerResponse struct {
|
||||||
Contacts []*Contact
|
Contacts []*Contact
|
||||||
Installations []*multidevice.Installation
|
Installations []*multidevice.Installation
|
||||||
|
@ -68,7 +62,6 @@ type MessengerResponse struct {
|
||||||
removedMessages map[string]*RemovedMessage
|
removedMessages map[string]*RemovedMessage
|
||||||
communities map[string]*communities.Community
|
communities map[string]*communities.Community
|
||||||
communitiesSettings map[string]*communities.CommunitySettings
|
communitiesSettings map[string]*communities.CommunitySettings
|
||||||
protectedTopics map[string]*ProtectedTopic
|
|
||||||
activityCenterNotifications map[string]*ActivityCenterNotification
|
activityCenterNotifications map[string]*ActivityCenterNotification
|
||||||
activityCenterState *ActivityCenterState
|
activityCenterState *ActivityCenterState
|
||||||
messages map[string]*common.Message
|
messages map[string]*common.Message
|
||||||
|
@ -110,7 +103,6 @@ func (r *MessengerResponse) MarshalJSON() ([]byte, error) {
|
||||||
Notifications []*localnotifications.Notification `json:"notifications"`
|
Notifications []*localnotifications.Notification `json:"notifications"`
|
||||||
Communities []*communities.Community `json:"communities,omitempty"`
|
Communities []*communities.Community `json:"communities,omitempty"`
|
||||||
CommunitiesSettings []*communities.CommunitySettings `json:"communitiesSettings,omitempty"`
|
CommunitiesSettings []*communities.CommunitySettings `json:"communitiesSettings,omitempty"`
|
||||||
ProtectedTopics []ProtectedTopic `json:"protectedTopics,omitempty"`
|
|
||||||
ActivityCenterNotifications []*ActivityCenterNotification `json:"activityCenterNotifications,omitempty"`
|
ActivityCenterNotifications []*ActivityCenterNotification `json:"activityCenterNotifications,omitempty"`
|
||||||
ActivityCenterState *ActivityCenterState `json:"activityCenterState,omitempty"`
|
ActivityCenterState *ActivityCenterState `json:"activityCenterState,omitempty"`
|
||||||
CurrentStatus *UserStatus `json:"currentStatus,omitempty"`
|
CurrentStatus *UserStatus `json:"currentStatus,omitempty"`
|
||||||
|
@ -152,7 +144,6 @@ func (r *MessengerResponse) MarshalJSON() ([]byte, error) {
|
||||||
Chats: r.Chats(),
|
Chats: r.Chats(),
|
||||||
Communities: r.Communities(),
|
Communities: r.Communities(),
|
||||||
CommunitiesSettings: r.CommunitiesSettings(),
|
CommunitiesSettings: r.CommunitiesSettings(),
|
||||||
ProtectedTopics: r.ProtectedTopics(),
|
|
||||||
RemovedChats: r.RemovedChats(),
|
RemovedChats: r.RemovedChats(),
|
||||||
RemovedMessages: r.RemovedMessages(),
|
RemovedMessages: r.RemovedMessages(),
|
||||||
ClearedHistories: r.ClearedHistories(),
|
ClearedHistories: r.ClearedHistories(),
|
||||||
|
@ -220,14 +211,6 @@ func (r *MessengerResponse) CommunitiesSettings() []*communities.CommunitySettin
|
||||||
return settings
|
return settings
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *MessengerResponse) ProtectedTopics() []ProtectedTopic {
|
|
||||||
protectedTopics := make([]ProtectedTopic, 0, len(r.protectedTopics))
|
|
||||||
for _, pt := range r.protectedTopics {
|
|
||||||
protectedTopics = append(protectedTopics, *pt)
|
|
||||||
}
|
|
||||||
return protectedTopics
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *MessengerResponse) Notifications() []*localnotifications.Notification {
|
func (r *MessengerResponse) Notifications() []*localnotifications.Notification {
|
||||||
var notifications []*localnotifications.Notification
|
var notifications []*localnotifications.Notification
|
||||||
for _, n := range r.notifications {
|
for _, n := range r.notifications {
|
||||||
|
@ -372,14 +355,6 @@ func (r *MessengerResponse) AddRequestsToJoinCommunity(requestsToJoin []*communi
|
||||||
r.RequestsToJoinCommunity = append(r.RequestsToJoinCommunity, requestsToJoin...)
|
r.RequestsToJoinCommunity = append(r.RequestsToJoinCommunity, requestsToJoin...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *MessengerResponse) AddProtectedTopic(pt *ProtectedTopic) {
|
|
||||||
if r.protectedTopics == nil {
|
|
||||||
r.protectedTopics = make(map[string]*ProtectedTopic)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.protectedTopics[pt.CommunityID] = pt
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *MessengerResponse) AddRequestToJoinCommunity(requestToJoin *communities.RequestToJoin) {
|
func (r *MessengerResponse) AddRequestToJoinCommunity(requestToJoin *communities.RequestToJoin) {
|
||||||
r.RequestsToJoinCommunity = append(r.RequestsToJoinCommunity, requestToJoin)
|
r.RequestsToJoinCommunity = append(r.RequestsToJoinCommunity, requestToJoin)
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,6 +104,9 @@ type ChannelGroup struct {
|
||||||
UnviewedMessagesCount int `json:"unviewedMessagesCount"`
|
UnviewedMessagesCount int `json:"unviewedMessagesCount"`
|
||||||
UnviewedMentionsCount int `json:"unviewedMentionsCount"`
|
UnviewedMentionsCount int `json:"unviewedMentionsCount"`
|
||||||
CheckChannelPermissionResponses map[string]*communities.CheckChannelPermissionsResponse `json:"checkChannelPermissionResponses"`
|
CheckChannelPermissionResponses map[string]*communities.CheckChannelPermissionsResponse `json:"checkChannelPermissionResponses"`
|
||||||
|
PubsubTopic string `json:"pubsubTopic"`
|
||||||
|
PubsubTopicKey string `json:"pubsubTopicKey"`
|
||||||
|
Shard *common.Shard `json:"shard"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAPI(service *Service) *API {
|
func NewAPI(service *Service) *API {
|
||||||
|
@ -234,6 +237,9 @@ func (api *API) getChannelGroups(ctx context.Context, channelGroupID string) (ma
|
||||||
UnviewedMessagesCount: totalUnviewedMessageCount,
|
UnviewedMessagesCount: totalUnviewedMessageCount,
|
||||||
UnviewedMentionsCount: totalUnviewedMentionsCount,
|
UnviewedMentionsCount: totalUnviewedMentionsCount,
|
||||||
CheckChannelPermissionResponses: make(map[string]*communities.CheckChannelPermissionsResponse),
|
CheckChannelPermissionResponses: make(map[string]*communities.CheckChannelPermissionsResponse),
|
||||||
|
PubsubTopic: community.PubsubTopic(),
|
||||||
|
PubsubTopicKey: community.PubsubTopicKey(),
|
||||||
|
Shard: community.Shard(),
|
||||||
}
|
}
|
||||||
|
|
||||||
for t, i := range community.Images() {
|
for t, i := range community.Images() {
|
||||||
|
|
Loading…
Reference in New Issue