Compare commits

...

32 Commits

Author SHA1 Message Date
Sarah Jamie Lewis 92a413815c Merge pull request 'Assert that group manager handle is a tor onion' (#566) from managed-groups into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #566
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2024-07-02 16:58:19 +00:00
Sarah Jamie Lewis 381a4b8e1e
Assert that group manager handle is a tor onion
continuous-integration/drone/pr Build is pending Details
2024-07-02 09:40:30 -07:00
Sarah Jamie Lewis 24f1e54cce Merge pull request 'Fix bug in file download when manifest fetch fails / Add group membership type api for Managed Groups' (#565) from managed-groups into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #565
2024-07-02 15:37:40 +00:00
Sarah Jamie Lewis 496139adfa Add API for Toggling Group Membership Type
continuous-integration/drone/pr Build is passing Details
2024-06-26 18:33:39 +00:00
Sarah Jamie Lewis fdb435fe83 Initiate redownload if manifest does not exist... 2024-06-26 18:33:39 +00:00
Sarah Jamie Lewis 9b235ba732 Merge pull request 'Gate the Import Bundle method for Managed Groups' (#564) from managed-groups into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #564
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2024-06-20 16:55:40 +00:00
Sarah Jamie Lewis 8b7cb44e44
Gate the Import Bundle method for Managed Groups
continuous-integration/drone/pr Build is passing Details
2024-06-18 12:57:21 -07:00
Dan Ballard d5145c631d createProfile return onion if possible; add attribute for private name
continuous-integration/drone/push Build is pending Details
2024-06-15 00:17:35 +00:00
Sarah Jamie Lewis 3e09a25b2d Merge pull request 'Managed Group Refinement' (#561) from managed-groups into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #561
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2024-06-13 17:48:35 +00:00
Sarah Jamie Lewis 229743c507
Managed Group Refinement
continuous-integration/drone/pr Build is passing Details
- Add NoAccessControl Fallback
- Prevent Facade Contacts from the Contact Retry Plugin
- Force Group Manager to Save History by Default
- Fix Ack'ing on Channel_Manager
2024-06-11 11:03:59 -07:00
Sarah Jamie Lewis c5aa6905a4 Merge pull request 'Managed Groups First Cut' (#558) from managed-groups into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #558
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2024-06-10 21:45:46 +00:00
Sarah Jamie Lewis 74d2aec96a
Fixup Channel Setting and Timeouts for Tests
continuous-integration/drone/pr Build is passing Details
2024-06-10 14:04:34 -07:00
Sarah Jamie Lewis 4bce08dc00
Add explicit check when sending offline to Manager Channel
continuous-integration/drone/pr Build is failing Details
2024-06-10 13:04:33 -07:00
Sarah Jamie Lewis 77c6139792
Add Comments indicating current status of hybrid groups
continuous-integration/drone/pr Build is failing Details
2024-06-10 12:15:14 -07:00
Sarah Jamie Lewis a35374f200
Revert
continuous-integration/drone/pr Build is failing Details
2024-06-10 11:08:42 -07:00
Sarah Jamie Lewis e14044e404
Add JSON Annotations
continuous-integration/drone/pr Build is passing Details
Also fix race condition in app map
2024-06-10 10:34:11 -07:00
Sarah Jamie Lewis fdec3302af
Clarify comments, add constants, clean up tests
continuous-integration/drone/pr Build is failing Details
2024-05-13 12:24:22 -07:00
Sarah Jamie Lewis d61dc30bb2
Managed Groups First Cut 2024-05-13 12:24:22 -07:00
Sarah Jamie Lewis a7b885166a Merge pull request 'Enable per-contact file sharing permissions' (#554) from ep into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #554
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2024-04-29 15:37:50 +00:00
Sarah Jamie Lewis b32b11c711
Enable per-contact file sharing permissions
continuous-integration/drone/pr Build is passing Details
2024-04-16 11:35:21 -07:00
Sarah Jamie Lewis 0e96539f22 Merge pull request 'Store Messages and Send when Online' (#553) from offline-messages into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #553
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2024-04-16 18:35:02 +00:00
Sarah Jamie Lewis e55f342324
Updating Logging -> Debug
continuous-integration/drone/pr Build is passing Details
2024-02-26 13:40:47 -08:00
Sarah Jamie Lewis 89aca91b37
Store Messages and Send when Online
continuous-integration/drone/pr Build is passing Details
2024-02-26 13:18:38 -08:00
Sarah Jamie Lewis cd918c02ea Merge pull request 'Fix Error in ACL-V1 that Prevented ShareFiles (for some)' (#552) from acl-v2 into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #552
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2024-02-26 17:26:17 +00:00
Sarah Jamie Lewis 05a198c89f
Fix Error in ACL-V1 that Prevented ShareFiles (for some)
continuous-integration/drone/pr Build is passing Details
Also aligns model.DeserializeAttributes to best practice
2024-02-24 12:51:19 -08:00
Sarah Jamie Lewis 1d9202ff93 Don't reject text messages
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is pending Details
2024-02-12 22:02:35 +00:00
Sarah Jamie Lewis 0907af57d5 Merge pull request 'Introduce Channel/Overlay Mappings' (#549) from overlays into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #549
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2024-02-11 23:10:59 +00:00
Sarah Jamie Lewis 826ac40a5c Stream check in engine
continuous-integration/drone/pr Build is pending Details
2024-02-11 14:45:11 -08:00
Sarah Jamie Lewis 1a034953df Util Functions for MW
continuous-integration/drone/pr Build is pending Details
2024-02-11 14:44:18 -08:00
Sarah Jamie Lewis 3124f7b7c4 MessageOverlay time to pointer
continuous-integration/drone/pr Build is pending Details
2024-02-11 13:56:19 -08:00
Sarah Jamie Lewis 792e79dceb Introduce Channel/Overlay Mappings
continuous-integration/drone/pr Build is failing Details
- Map channel 7 to ephemeral / no ack
- Create model methods
- Introduce optional latency measurements into Cwtch
2024-02-11 12:14:07 -08:00
Sarah Jamie Lewis 3e0680943a Prevent Duplicate Queue Subscription
continuous-integration/drone/pr Build is pending Details
continuous-integration/drone/push Build is failing Details
2024-02-09 13:16:23 -08:00
33 changed files with 1499 additions and 118 deletions

4
.gitignore vendored
View File

@ -33,4 +33,6 @@ data-dir-cwtchtool/
tokens
tordir/
testing/autodownload/download_dir
testing/autodownload/storage
testing/autodownload/storage
*.swp
testing/managerstorage/*

View File

@ -1,10 +1,16 @@
package app
import (
"os"
path "path/filepath"
"strconv"
"sync"
"cwtch.im/cwtch/app/plugins"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/extensions"
"cwtch.im/cwtch/functionality/filesharing"
"cwtch.im/cwtch/functionality/hybrid"
"cwtch.im/cwtch/functionality/servers"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
@ -15,10 +21,6 @@ import (
"cwtch.im/cwtch/storage"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"os"
path "path/filepath"
"strconv"
"sync"
)
type application struct {
@ -51,7 +53,7 @@ func (app *application) IsFeatureEnabled(experiment string) bool {
// Application is a full cwtch peer application. It allows management, usage and storage of multiple peers
type Application interface {
LoadProfiles(password string)
CreateProfile(name string, password string, autostart bool)
CreateProfile(name string, password string, autostart bool) string
InstallEngineHooks(engineHooks connections.EngineHooks)
ImportProfile(exportedCwtchFile string, password string) (peer.CwtchPeer, error)
EnhancedImportProfile(exportedCwtchFile string, password string) string
@ -195,7 +197,7 @@ func (app *application) AddPlugin(peerid string, id plugins.PluginID, bus event.
}
}
func (app *application) CreateProfile(name string, password string, autostart bool) {
func (app *application) CreateProfile(name string, password string, autostart bool) string {
autostartVal := constants.True
if !autostart {
autostartVal = constants.False
@ -205,10 +207,15 @@ func (app *application) CreateProfile(name string, password string, autostart bo
tagVal = constants.ProfileTypeV1DefaultPassword
}
app.CreatePeer(name, password, map[attr.ZonedPath]string{
profile_id, err := app.CreatePeer(name, password, map[attr.ZonedPath]string{
attr.ProfileZone.ConstructZonedPath(constants.Tag): tagVal,
attr.ProfileZone.ConstructZonedPath(constants.PeerAutostart): autostartVal,
})
if err == nil {
return profile_id
}
return ""
}
func (app *application) setupPeer(profile peer.CwtchPeer) {
@ -230,7 +237,7 @@ func (app *application) setupPeer(profile peer.CwtchPeer) {
}
func (app *application) CreatePeer(name string, password string, attributes map[attr.ZonedPath]string) {
func (app *application) CreatePeer(name string, password string, attributes map[attr.ZonedPath]string) (string, error) {
app.appmutex.Lock()
defer app.appmutex.Unlock()
@ -240,7 +247,7 @@ func (app *application) CreatePeer(name string, password string, attributes map[
if err != nil {
log.Errorf("Error Creating Peer: %v", err)
app.appBus.Publish(event.NewEventList(event.PeerError, event.Error, err.Error()))
return
return "", err
}
app.setupPeer(profile)
@ -251,6 +258,7 @@ func (app *application) CreatePeer(name string, password string, attributes map[
}
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.GetOnion(), event.Created: event.True}))
return profile.GetOnion(), nil
}
func (app *application) DeleteProfile(onion string, password string) {
@ -363,9 +371,12 @@ func (app *application) LoadProfiles(password string) {
func (app *application) registerHooks(profile peer.CwtchPeer) {
// Register Hooks
profile.RegisterHook(extensions.ProfileValueExtension{})
profile.RegisterHook(extensions.SendWhenOnlineExtension{})
profile.RegisterHook(new(filesharing.Functionality))
profile.RegisterHook(new(filesharing.ImagePreviewsFunctionality))
profile.RegisterHook(new(servers.Functionality))
profile.RegisterHook(new(hybrid.ManagedGroupFunctionality))
profile.RegisterHook(new(hybrid.GroupManagerFunctionality)) // will only be activated if GroupManagerExperiment is enabled...
// Ensure that Profiles have the Most Up to Date Settings...
profile.NotifySettingsUpdate(app.settings.ReadGlobalSettings())
}
@ -391,12 +402,14 @@ func (app *application) installProfile(profile peer.CwtchPeer) bool {
func (app *application) ActivatePeerEngine(onion string) {
profile := app.GetPeer(onion)
if profile != nil {
app.appmutex.Lock()
if _, exists := app.engines[onion]; !exists {
eventBus, exists := app.eventBuses[profile.GetOnion()]
if !exists {
// todo handle this case?
log.Errorf("cannot activate peer engine without an event bus")
app.appmutex.Unlock()
return
}
@ -405,12 +418,13 @@ func (app *application) ActivatePeerEngine(onion string) {
log.Debugf("restartFlow: Creating a New Protocol Engine...")
app.engines[profile.GetOnion()] = engine
eventBus.Publish(event.NewEventList(event.ProtocolEngineCreated))
app.QueryACNStatus()
} else {
log.Errorf("corrupted profile detected for %v", onion)
}
}
app.appmutex.Unlock()
}
app.QueryACNStatus()
}
// ConfigureConnections autostarts the given kinds of connections.
@ -418,7 +432,9 @@ func (app *application) ConfigureConnections(onion string, listen bool, peers bo
profile := app.GetPeer(onion)
if profile != nil {
app.appmutex.Lock()
profileBus, exists := app.eventBuses[profile.GetOnion()]
app.appmutex.Unlock()
if exists {
// if we are making a decision to ignore
if !peers || !servers {

View File

@ -284,6 +284,7 @@ const (
Status = Field("Status")
EventID = Field("EventID")
EventContext = Field("EventContext")
Channel = Field("Channel")
Index = Field("Index")
RowIndex = Field("RowIndex")
ContentHash = Field("ContentHash")

View File

@ -95,6 +95,11 @@ func (em *manager) initialize() {
func (em *manager) Subscribe(eventType Type, queue Queue) {
em.mapMutex.Lock()
defer em.mapMutex.Unlock()
for _, sub := range em.subscribers[eventType] {
if sub == queue {
return // don't add the same queue for the same event twice...
}
}
em.subscribers[eventType] = append(em.subscribers[eventType], queue)
}

View File

@ -104,6 +104,15 @@ func (pne ProfileValueExtension) OnContactRequestValue(profile peer.CwtchPeer, c
val, exists = profile.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
}
// NOTE: Cwtch 1.15+ requires that profiles be able to restrict file downloading to specific contacts. As such we need an ACL check here
// on the fileshareing zone.
// TODO: Split this functionality into FilesharingFunctionality, and restrict this function to only considering Profile zoned attributes?
if zone == attr.FilesharingZone {
if !conversation.GetPeerAC().ShareFiles {
return
}
}
// Construct a Response
resp := event.NewEvent(event.SendRetValMessageToPeer, map[event.Field]string{event.ConversationID: strconv.Itoa(conversation.ID), event.RemotePeer: conversation.Handle, event.Exists: strconv.FormatBool(exists)})
resp.EventID = eventID

View File

@ -0,0 +1,91 @@
package extensions
import (
"slices"
"strconv"
"time"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/protocol/connections"
"cwtch.im/cwtch/settings"
"git.openprivacy.ca/openprivacy/log"
)
// SendWhenOnlineExtension implements automatic sending
// Some Considerations:
// - There are race conditions inherant in this approach e.g. a peer could go offline just after recieving a message and never sending an ack
// - In that case the next time we connect we will send a duplicate message.
// - Currently we do not include metadata like sent time in raw peer protocols (however Overlay does now have support for that information)
type SendWhenOnlineExtension struct {
}
func (soe SendWhenOnlineExtension) NotifySettingsUpdate(_ settings.GlobalSettings) {
}
func (soe SendWhenOnlineExtension) EventsToRegister() []event.Type {
return []event.Type{event.PeerStateChange}
}
func (soe SendWhenOnlineExtension) ExperimentsToRegister() []string {
return nil
}
func (soe SendWhenOnlineExtension) OnEvent(ev event.Event, profile peer.CwtchPeer) {
switch ev.EventType {
case event.PeerStateChange:
ci, err := profile.FetchConversationInfo(ev.Data["RemotePeer"])
if err == nil {
// if we have re-authenticated with thie peer then request their profile image...
if connections.ConnectionStateToType()[ev.Data[event.ConnectionState]] == connections.AUTHENTICATED {
log.Infof("Sending Offline Messages to %s", ci.Handle)
// Check the last 100 messages, if any of them are pending, then send them now...
messsages, _ := profile.GetMostRecentMessages(ci.ID, constants.CHANNEL_CHAT, 0, uint(100))
slices.Reverse(messsages)
for _, message := range messsages {
if message.Attr[constants.AttrAck] == constants.False {
sent, timeparseerr := time.Parse(time.RFC3339, message.Attr[constants.AttrSentTimestamp])
if timeparseerr != nil {
continue
}
if time.Since(sent) > time.Hour*24*7 {
continue
}
body := message.Body
ev := event.NewEvent(event.SendMessageToPeer, map[event.Field]string{event.ConversationID: strconv.Itoa(ci.ID), event.RemotePeer: ci.Handle, event.Data: body})
ev.EventID = message.Signature // we need this ensure that we correctly ack this in the db when it comes back
// TODO: The EventBus is becoming very noisy...we may want to consider a one-way shortcut to Engine i.e. profile.Engine.SendMessageToPeer
log.Infof("resending message that was sent when peer was offline")
profile.PublishEvent(ev)
}
}
if ci.HasChannel(constants.CHANNEL_MANAGER) {
messsages, _ = profile.GetMostRecentMessages(ci.ID, constants.CHANNEL_MANAGER, 0, uint(100))
slices.Reverse(messsages)
for _, message := range messsages {
if message.Attr[constants.AttrAck] == constants.False {
body := message.Body
ev := event.NewEvent(event.SendMessageToPeer, map[event.Field]string{event.ConversationID: strconv.Itoa(ci.ID), event.RemotePeer: ci.Handle, event.Data: body})
ev.EventID = message.Signature // we need this ensure that we correctly ack this in the db when it comes back
// TODO: The EventBus is becoming very noisy...we may want to consider a one-way shortcut to Engine i.e. profile.Engine.SendMessageToPeer
log.Debugf("resending message that was sent when peer was offline")
profile.PublishEvent(ev)
}
}
}
}
}
}
}
// OnContactReceiveValue is nop for SendWhenOnnlineExtension
func (soe SendWhenOnlineExtension) OnContactReceiveValue(profile peer.CwtchPeer, conversation model.Conversation, szp attr.ScopedZonedPath, value string, exists bool) {
}
// OnContactRequestValue is nop for SendWhenOnnlineExtension
func (soe SendWhenOnlineExtension) OnContactRequestValue(profile peer.CwtchPeer, conversation model.Conversation, eventID string, szp attr.ScopedZonedPath) {
}

View File

@ -200,11 +200,10 @@ func (f *Functionality) VerifyOrResumeDownload(profile peer.CwtchPeer, conversat
profile.PublishEvent(event.NewEvent(event.FileDownloaded, map[event.Field]string{event.FileKey: fileKey, event.FilePath: downloadfilepath, event.TempFile: downloadfilepath}))
// File is verified and there is nothing else to do...
return nil
} else {
// Kick off another Download...
return f.DownloadFile(profile, conversation, downloadfilepath, manifestFilePath, fileKey, size)
}
}
// The manifest is corrupted, we need to fetch it again...
return f.DownloadFile(profile, conversation, downloadfilepath, manifestFilePath, fileKey, size)
}
}
return errors.New("file download metadata does not exist, or is corrupted")

View File

@ -62,7 +62,15 @@ func (i *ImagePreviewsFunctionality) OnEvent(ev event.Event, profile peer.CwtchP
if err == nil {
for _, ci := range conversations {
if profile.GetPeerState(ci.Handle) == connections.AUTHENTICATED {
profile.SendScopedZonedGetValToContact(ci.ID, attr.PublicScope, attr.ProfileZone, constants.CustomProfileImageKey)
// if we have enabled file shares for this contact, then send them our profile image
// NOTE: In the past, Cwtch treated "profile image" as a public file share. As such, anyone with the file key and who is able
// to authenticate with the profile (i.e. non-blocked peers) can download the file (if the global profile images experiment is enabled)
// To better allow for fine-grained permissions (and to support hybrid group permissions), we want to enable per-conversation file
// sharing permissions. As such, profile images are now only shared with contacts with that permission enabled.
// (i.e. all previous accepted contacts, new accepted contacts, and contacts who have this toggle set explictly)
if ci.GetPeerAC().ShareFiles {
profile.SendScopedZonedGetValToContact(ci.ID, attr.PublicScope, attr.ProfileZone, constants.CustomProfileImageKey)
}
}
}
}

View File

@ -0,0 +1,107 @@
package hybrid
import (
"crypto/ed25519"
"encoding/base32"
"encoding/json"
"fmt"
"strings"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"git.openprivacy.ca/openprivacy/log"
)
const ManagedGroupOpen = "managed-group-open"
type GroupEventType int
const (
MemberGroupIDKey = "member_group_id_key"
MemberMessageIDKey = "member_group_messge_id"
)
const (
AddMember = GroupEventType(0x1000)
RemoveMember = GroupEventType(0x2000)
RotateKey = GroupEventType(0x3000)
NewMessage = GroupEventType(0x4000)
NewClearMessage = GroupEventType(0x5000)
SyncRequest = GroupEventType(0x6000)
)
type ManageGroupEvent struct {
EventType GroupEventType `json:"t"`
Data string `json:"d"` // json encoded data
}
type AddMemberEvent struct {
Handle string `json:"h"`
}
type RemoveMemberEvent struct {
Handle string `json:"h"`
}
type RotateKeyEvent struct {
Key []byte `json:"k"`
}
type NewMessageEvent struct {
EncryptedHybridGroupMessage []byte `json:"m"`
}
type NewClearMessageEvent struct {
HybridGroupMessage HybridGroupMessage `json:"m"`
}
type SyncRequestMessage struct {
// a map of MemberGroupID: MemberMessageID
LastSeen map[int]int `json:"l"`
}
// This file contains code for the Hybrid Group / Managed Group types..
type HybridGroupMessage struct {
Author string `json:"a"` // the authors cwtch address
MemberGroupID uint32 `json:"g"`
MemberMessageID uint32 `json:"m"`
MessageBody string `json:"b"`
Sent uint64 `json:"t"` // milliseconds since epoch
Signature []byte `json:"s"` // of json-encoded content (including empty sig)
}
// AuthenticateMessage returns true if the Author of the message produced the Signature over the message
func AuthenticateMessage(message HybridGroupMessage) bool {
messageCopy := message
messageCopy.Signature = []byte{}
// Otherwise we derive the public key from the sender and check it against that.
decodedPub, err := base32.StdEncoding.DecodeString(strings.ToUpper(message.Author))
if err == nil {
data, err := json.Marshal(messageCopy)
if err == nil && len(decodedPub) >= 32 {
return ed25519.Verify(decodedPub[:32], data, message.Signature)
}
}
log.Errorf("invalid signature on message from %s", message)
return false
}
func CheckACL(handle string, group *model.Conversation) (*model.AccessControl, error) {
if isOpen, exists := group.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(ManagedGroupOpen)).ToString()]; !exists {
return nil, fmt.Errorf("group has not been setup correctly - ManagedGroupOpen does not exist ")
} else if isOpen == event.True {
// We don't need to do a membership check
defaultACL := group.GetPeerAC()
return &defaultACL, nil
}
// If this is a closed group. Check if we have an ACL entry for this member
// If we don't OR that member has been blocked, then close the connection.
if acl, inGroup := group.ACL[handle]; !inGroup || acl.Blocked {
log.Infof("ACL Check Failed: %v %v %v", handle, acl, inGroup)
return nil, fmt.Errorf("peer is not a member of this group")
} else {
return &acl, nil
}
}

View File

@ -0,0 +1,245 @@
// This file contains all code related to how a Group Manager operates over a group.
// Managed groups are canonically controlled by members setting
// the ManageGroup permission in the conversation ACL; allowing the manager to
// take control of how this group is structured, see OnEvent below...
// TODO: This file represents stage 1 of the roll out which de-risks most of the
// integration into cwtch peer, new interfaces, and UI integration
// The following functionality is not yet implemented:
// - group-level encryption
// - key rotation / membership ACL
// Cwtch Hybrid Groups are still very experimental functionality and should
// only be used for testing purposes.
package hybrid
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/protocol/connections"
"cwtch.im/cwtch/settings"
"encoding/json"
"fmt"
"git.openprivacy.ca/openprivacy/log"
)
// MANAGED_GROUP_HANDLE denotes the nominal name that the managed group is given, for easier handling
// Note: we could use id here, as the managed group should technically always be the first group
// But we don't want to assume that, and also allow conversations to be moved around without
// constantly referring to a magic id.
const MANAGED_GROUP_HANDLE = "managed:000"
type GroupManagerFunctionality struct {
}
func (f *GroupManagerFunctionality) NotifySettingsUpdate(settings settings.GlobalSettings) {
}
func (f *GroupManagerFunctionality) EventsToRegister() []event.Type {
return []event.Type{event.PeerStateChange, event.NewMessageFromPeerEngine}
}
func (f *GroupManagerFunctionality) ExperimentsToRegister() []string {
return []string{constants.GroupManagerExperiment, constants.GroupsExperiment}
}
// OnEvent handles File Sharing Hooks like Manifest Received and FileDownloaded
func (f *GroupManagerFunctionality) OnEvent(ev event.Event, profile peer.CwtchPeer) {
// We only want to engage this functionality if the peer is managing a group.
// In that case ALL peer connections and messages need to be routed through
// the management logic
// For now, we assume that a manager is a peer with a special management group.
// In the future we may want to make this a profile-level switch/attribute.
isManager := false
if ci, err := profile.FetchConversationInfo(MANAGED_GROUP_HANDLE); ci != nil && err == nil {
isManager = true
}
if isManager {
switch ev.EventType {
case event.PeerStateChange:
handle := ev.Data["RemotePeer"]
// check that we have authenticated with this peer
if connections.ConnectionStateToType()[ev.Data[event.ConnectionState]] == connections.AUTHENTICATED {
mg, err := f.GetManagedGroup(profile)
if err != nil {
log.Infof("group manager received peer connections but no suitable group has been found: %v %v", handle, err)
profile.DisconnectFromPeer(handle)
break
}
if _, err := CheckACL(handle, mg); err != nil {
log.Infof("received managed group connection from unauthorized peer: %v %v", handle, err)
profile.DisconnectFromPeer(handle)
break
}
}
// This is where most of the magic happens for managed groups. A few notes:
// - CwtchPeer has already taken care of storing this for us, we don't need to worry about that
// - Group Managers **only** speak overlays and **always** wrap their messages in a ManageGroupEvent anything else is fast-rejected.
case event.NewMessageFromPeerEngine:
log.Infof("received new message from peer: manager")
ci, err := f.GetManagedGroup(profile)
if err != nil {
log.Errorf("unknown conversation %v", err)
break // we don't care about unknown conversations...
}
var cm model.MessageWrapper
err = json.Unmarshal([]byte(ev.Data[event.Data]), &cm)
if err != nil {
log.Errorf("could not deserialize json %s %v", ev.Data[event.Data], err)
break
}
// The overlay type of this message **must** be ManageGroupEvent
if cm.Overlay == model.OverlayManageGroupEvent {
var mge ManageGroupEvent
err = json.Unmarshal([]byte(cm.Data), &mge)
if err == nil {
f.handleEvent(profile, *ci, mge, ev.Data[event.Data])
}
}
}
}
}
// handleEvent takes in a high level ManageGroupEvent message, transforms it into the proper type, and passes it on for handling
// assumes we are called after an event provided by an authorized peer (i.e. ManageGroup == true)
func (f *GroupManagerFunctionality) handleEvent(profile peer.CwtchPeer, conversation model.Conversation, mge ManageGroupEvent, original string) {
switch mge.EventType {
case NewClearMessage:
var nme NewClearMessageEvent
err := json.Unmarshal([]byte(mge.Data), &nme)
if err == nil {
f.handleNewMessageEvent(profile, conversation, nme, original)
}
}
}
func (f *GroupManagerFunctionality) handleNewMessageEvent(profile peer.CwtchPeer, conversation model.Conversation, nme NewClearMessageEvent, original string) {
log.Infof("handling new clear message event")
hgm := nme.HybridGroupMessage
if AuthenticateMessage(hgm) {
log.Infof("authenticated message")
group, err := f.GetManagedGroup(profile)
if err != nil {
log.Infof("received fraudulant hybrid message from group: %v", err)
return
}
if acl, err := CheckACL(hgm.Author, group); err != nil {
log.Infof("received fraudulant hybrid message from group: %v", err)
return
} else if !acl.Append {
log.Infof("received fraudulant hybrid message from group: peer does not have append privileges")
return
} else {
// TODO - Store this message locally in a format that makes it easier to
// do assurance later on
// forward the message to everyone who the server has added as a contact
// and who are represented in the ACL...
allConversations, _ := profile.FetchConversations()
for _, ci := range allConversations {
// NOTE: This check works for Open Groups too as CheckACL will return the default ACL
// for the group....
if ci.Handle != MANAGED_GROUP_HANDLE { // don't send to ourselves...
if acl, err := CheckACL(hgm.Author, group); err == nil && acl.Read {
log.Infof("forwarding group message to: %v", ci.Handle)
profile.SendMessage(ci.ID, original)
}
}
}
}
} else {
log.Errorf("received fraudulant hybrid message fom group")
}
}
// GetManagedGroup is a convieniance function that looks up the managed group
func (f *GroupManagerFunctionality) GetManagedGroup(profile peer.CwtchPeer) (*model.Conversation, error) {
return profile.FetchConversationInfo(MANAGED_GROUP_HANDLE)
}
// Establish a new Managed Group and return its conversation id
func (f *GroupManagerFunctionality) ManageNewGroup(profile peer.CwtchPeer) (int, error) {
// note: a manager can only manage one group. This will (probably) always be true and has a few benefits
// and downsides.
// The main downside is that it requires a new manager per group (and thus an onion service per group)
// However, it means that we can lean on p2p functionality like profile images / metadata / name
// etc. for group metadata and effectively get that for-free in the client.
// HOWEVER: hedging our bets here by giving this group a numeric handle...
if _, err := profile.FetchConversationInfo(MANAGED_GROUP_HANDLE); err == nil {
return -1, fmt.Errorf("manager is already managing a group")
}
ac := model.DefaultP2PAccessControl()
// by setting the ManageGroup permission in this ACL we are allowing the manager to
// take control of how this group is structured, see OnEvent above...
ac.ManageGroup = true
acl := model.AccessControlList{}
acl[profile.GetOnion()] = ac
acl[MANAGED_GROUP_HANDLE] = model.NoAccessControl()
ci, err := profile.NewConversation(MANAGED_GROUP_HANDLE, acl)
if err != nil {
return -1, err
}
profile.SetConversationAttribute(ci, attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(ManagedGroupOpen)), event.False)
return ci, nil
}
func (f *GroupManagerFunctionality) SetMembershipOpen(profile peer.CwtchPeer) error {
if ci, err := profile.FetchConversationInfo(MANAGED_GROUP_HANDLE); err != nil {
return fmt.Errorf("manager is already managing a group")
} else {
profile.SetConversationAttribute(ci.ID, attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(ManagedGroupOpen)), event.True)
return nil
}
}
func (f *GroupManagerFunctionality) SetMembershipClosed(profile peer.CwtchPeer) error {
if ci, err := profile.FetchConversationInfo(MANAGED_GROUP_HANDLE); err != nil {
return fmt.Errorf("manager is already managing a group")
} else {
profile.SetConversationAttribute(ci.ID, attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(ManagedGroupOpen)), event.True)
return nil
}
}
// AddHybridContact is a wrapper arround NewContactConversation which sets the contact
// up for Hybrid Group channel messages...
// TODO this function assumes that authorization has been done at a higher level..
func (f *GroupManagerFunctionality) AddHybridContact(profile peer.CwtchPeer, handle string) error {
ac := model.DefaultP2PAccessControl()
ac.ManageGroup = false
ci, err := profile.NewContactConversation(handle, ac, true)
if err != nil {
return err
}
mg, err := f.GetManagedGroup(profile)
if err != nil {
return err
}
// Update the ACL list to add this contact...
acl := mg.ACL
acl[handle] = model.DefaultP2PAccessControl()
profile.UpdateConversationAccessControlList(mg.ID, acl)
// enable channel 2 on this conversation (hybrid groups management channel)
profile.InitChannel(ci, constants.CHANNEL_MANAGER)
key := fmt.Sprintf("channel.%d", constants.CHANNEL_MANAGER)
profile.SetConversationAttribute(ci, attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(key)), constants.True)
// Group managers need to always save history (and manually deal with purging...)
profile.SetConversationAttribute(ci, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(event.SaveHistoryKey)), event.SaveHistoryConfirmed)
return nil
}
func (f *GroupManagerFunctionality) OnContactRequestValue(profile peer.CwtchPeer, conversation model.Conversation, eventID string, path attr.ScopedZonedPath) {
// nop hybrid group conversations do not exchange contact requests
}
func (f *GroupManagerFunctionality) OnContactReceiveValue(profile peer.CwtchPeer, conversation model.Conversation, path attr.ScopedZonedPath, value string, exists bool) {
// nop hybrid group conversations do not exchange contact requests
}

View File

@ -0,0 +1,336 @@
package hybrid
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"math"
"math/big"
"strconv"
"time"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/settings"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
"golang.org/x/crypto/nacl/secretbox"
)
type ManagedGroupFunctionality struct {
}
func (f ManagedGroupFunctionality) NotifySettingsUpdate(settings settings.GlobalSettings) {
}
func (f ManagedGroupFunctionality) EventsToRegister() []event.Type {
return []event.Type{event.NewMessageFromPeerEngine}
}
func (f ManagedGroupFunctionality) ExperimentsToRegister() []string {
return []string{constants.GroupsExperiment}
}
// OnEvent handles File Sharing Hooks like Manifest Received and FileDownloaded
func (f *ManagedGroupFunctionality) OnEvent(ev event.Event, profile peer.CwtchPeer) {
switch ev.EventType {
// This is where most of the magic happens for managed groups. A few notes:
// - CwtchPeer has already taken care of storing this for us, we don't need to worry about that
// - Group Managers **only** speak overlays and **always** wrap their messages in a ManageGroupEvent anything else is fast-rejected.
case event.NewMessageFromPeerEngine:
handle := ev.Data[event.RemotePeer]
ci, err := profile.FetchConversationInfo(handle)
if err != nil {
break // we don't care about unknown conversations...
}
// We reject managed group requests for groups not setup as managed groups...
if ci.ACL[handle].ManageGroup {
var cm model.MessageWrapper
err = json.Unmarshal([]byte(ev.Data[event.Data]), &cm)
if err != nil {
break
}
// The overlay type of this message **must** be ManageGroupEvent
if cm.Overlay == model.OverlayManageGroupEvent {
var mge ManageGroupEvent
err = json.Unmarshal([]byte(cm.Data), &mge)
if err == nil {
cid, err := profile.FetchConversationInfo(handle)
if err == nil {
f.handleEvent(profile, *cid, mge)
}
}
}
}
}
}
// handleEvent takes in a high level ManageGroupEvent message, transforms it into the proper type, and passes it on for handling
// assumes we are called after an event provided by an authorized peer (i.e. ManageGroup == true)
func (f *ManagedGroupFunctionality) handleEvent(profile peer.CwtchPeer, conversation model.Conversation, mge ManageGroupEvent) {
switch mge.EventType {
case AddMember:
var ame AddMemberEvent
err := json.Unmarshal([]byte(mge.Data), &ame)
if err == nil {
f.handleAddMemberEvent(profile, conversation, ame)
}
case RemoveMember:
var rme RemoveMemberEvent
err := json.Unmarshal([]byte(mge.Data), &rme)
if err == nil {
f.handleRemoveMemberEvent(profile, conversation, rme)
}
case NewMessage:
var nme NewMessageEvent
err := json.Unmarshal([]byte(mge.Data), &nme)
if err == nil {
f.handleNewMessageEvent(profile, conversation, nme)
}
case NewClearMessage:
var nme NewClearMessageEvent
err := json.Unmarshal([]byte(mge.Data), &nme)
if err == nil {
f.handleNewClearMessageEvent(profile, conversation, nme)
}
case RotateKey:
var rke RotateKeyEvent
err := json.Unmarshal([]byte(mge.Data), &rke)
if err == nil {
f.handleRotateKeyEvent(profile, conversation, rke)
}
}
}
// handleAddMemberEvent adds a group member to the conversation ACL
// assumes we are called after an event provided by an authorized peer (i.e. ManageGroup == true)
func (f *ManagedGroupFunctionality) handleAddMemberEvent(profile peer.CwtchPeer, conversation model.Conversation, ame AddMemberEvent) {
acl := conversation.ACL
acl[ame.Handle] = model.DefaultP2PAccessControl()
profile.UpdateConversationAccessControlList(conversation.ID, acl)
}
// handleRemoveMemberEvent removes a group member from the conversation ACL
// assumes we are called after an event provided by an authorized peer (i.e. ManageGroup == true)
func (f *ManagedGroupFunctionality) handleRemoveMemberEvent(profile peer.CwtchPeer, conversation model.Conversation, rme RemoveMemberEvent) {
acl := conversation.ACL
delete(acl, rme.Handle)
profile.UpdateConversationAccessControlList(conversation.ID, acl)
}
// handleRotateKeyEvent rotates the encryption key for a given group
// assumes we are called after an event provided by an authorized peer (i.e. ManageGroup == true)
// TODO this currently is a noop as group levle encryption is unimplemented
func (f *ManagedGroupFunctionality) handleRotateKeyEvent(profile peer.CwtchPeer, conversation model.Conversation, rke RotateKeyEvent) {
keyScope := attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath("key"))
keyB64 := base64.StdEncoding.EncodeToString(rke.Key)
profile.SetConversationAttribute(conversation.ID, keyScope, keyB64)
}
// TODO this is a sketch implementation that is not yet complete.
func (f *ManagedGroupFunctionality) handleNewMessageEvent(profile peer.CwtchPeer, conversation model.Conversation, nme NewMessageEvent) {
keyScope := attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath("key"))
if keyB64, err := profile.GetConversationAttribute(conversation.ID, keyScope); err == nil {
key, err := base64.StdEncoding.DecodeString(keyB64)
if err != nil || len(key) != 32 {
log.Errorf("hybrid group key is corrupted")
return
}
// decrypt the message with key...
hgm, err := f.decryptMessage(key, nme.EncryptedHybridGroupMessage)
if hgm == nil || err != nil {
log.Errorf("unable to decrypt hybrid group message: %v", err)
return
}
f.handleNewClearMessageEvent(profile, conversation, NewClearMessageEvent{HybridGroupMessage: *hgm})
}
}
func (f *ManagedGroupFunctionality) handleNewClearMessageEvent(profile peer.CwtchPeer, conversation model.Conversation, nme NewClearMessageEvent) {
hgm := nme.HybridGroupMessage
if AuthenticateMessage(hgm) {
// TODO Closed Group Membership Check - right now we only support open groups...
if profile.GetOnion() == hgm.Author {
// ack
signatureB64 := base64.StdEncoding.EncodeToString(hgm.Signature)
id, err := profile.GetChannelMessageBySignature(conversation.ID, constants.CHANNEL_CHAT, signatureB64)
if err == nil {
profile.UpdateMessageAttribute(conversation.ID, constants.CHANNEL_CHAT, id, constants.AttrAck, constants.True)
profile.PublishEvent(event.NewEvent(event.IndexedAcknowledgement, map[event.Field]string{event.ConversationID: strconv.Itoa(conversation.ID), event.Index: strconv.Itoa(id)}))
}
} else {
mgidstr := strconv.Itoa(int(nme.HybridGroupMessage.MemberGroupID)) // we need both MemberGroupId and MemberMessageId for attestation later on...
newmmidstr := strconv.Itoa(int(nme.HybridGroupMessage.MemberMessageID))
// Set the attributes of this message...
attr := model.Attributes{MemberGroupIDKey: mgidstr, MemberMessageIDKey: newmmidstr,
constants.AttrAuthor: hgm.Author,
constants.AttrAck: event.True,
constants.AttrSentTimestamp: time.UnixMilli(int64(hgm.Sent)).Format(time.RFC3339Nano)}
// Note: The Channel here is 0...this is the main channel that UIs understand as the default, so this message is
// becomes part of the conversation...
mid, err := profile.InternalInsertMessage(conversation.ID, constants.CHANNEL_CHAT, hgm.Author, hgm.MessageBody, attr, hgm.Signature)
contenthash := model.CalculateContentHash(hgm.Author, hgm.MessageBody)
if err == nil {
profile.PublishEvent(event.NewEvent(event.NewMessageFromGroup, map[event.Field]string{event.ConversationID: strconv.Itoa(conversation.ID), event.TimestampSent: time.UnixMilli(int64(hgm.Sent)).Format(time.RFC3339Nano), event.RemotePeer: hgm.Author, event.Index: strconv.Itoa(mid), event.Data: hgm.MessageBody, event.ContentHash: contenthash}))
}
}
// TODO need to send an event here...
} else {
log.Errorf("received fraudulant hybrid message fom group")
}
}
// todo sketch function
func (f *ManagedGroupFunctionality) decryptMessage(key []byte, ciphertext []byte) (*HybridGroupMessage, error) {
if len(ciphertext) > 24 {
var decryptNonce [24]byte
copy(decryptNonce[:], ciphertext[:24])
var fixedSizeKey [32]byte
copy(fixedSizeKey[:], key[:32])
decrypted, ok := secretbox.Open(nil, ciphertext[24:], &decryptNonce, &fixedSizeKey)
if ok {
var hgm HybridGroupMessage
err := json.Unmarshal(decrypted, &hgm)
return &hgm, err
}
}
return nil, fmt.Errorf("invalid ciphertext/key error")
}
// Define a new managed group, managed by the manager...
func (f *ManagedGroupFunctionality) NewManagedGroup(profile peer.CwtchPeer, manager string) error {
if !tor.IsValidHostname(manager) {
return fmt.Errorf("manager handle must be a tor v3 onion")
}
// generate a truely random member id for this group in [0..2^32)
nBig, err := rand.Int(rand.Reader, big.NewInt(math.MaxUint32))
if err != nil {
return err // if there is a problem with random we want to exit now rather than have to clean up group setup...
}
ac := model.DefaultP2PAccessControl()
ac.ManageGroup = true // by setting the ManageGroup permission in this ACL we are allowing the manager to control of how this group is structured
ci, err := profile.NewContactConversation(manager, ac, true)
if err != nil {
return err
}
// enable channel 2 on this conversation (hybrid groups management channel)
key := fmt.Sprintf("channel.%d", 2)
err = profile.SetConversationAttribute(ci, attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(key)), constants.True)
if err != nil {
return fmt.Errorf("could not enable channel 2 on hybrid group: %v", err) // likely a catestrophic error...fail
}
err = profile.InitChannel(ci, 2)
if err != nil {
return fmt.Errorf("could not enable channel 2 on hybrid group: %v", err) // likely a catestrophic error...fail
}
// finally, set the member group id on this group...
mgidkey := attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(MemberGroupIDKey))
err = profile.SetConversationAttributeInt(ci, mgidkey, int(nBig.Uint64()))
if err != nil {
return fmt.Errorf("could not set group id on hybrid group: %v", err) // likely a catestrophic error...fail
}
return nil
}
// SendMessageToManagedGroup acts like SendMessage(ToPeer), but with a few additional bookkeeping steps for Hybrid Groups
func (f *ManagedGroupFunctionality) SendMessageToManagedGroup(profile peer.CwtchPeer, conversation int, message string) (int, error) {
mgidkey := attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(MemberGroupIDKey))
mgid, err := profile.GetConversationAttributeInt(conversation, mgidkey)
if err != nil {
return -1, err
}
mmidkey := attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(MemberMessageIDKey))
mmid, err := profile.GetConversationAttributeInt(conversation, mmidkey)
if err != nil {
mmid = 0 // first message
}
mmid += 1
// Now time to package this whole thing in layers of JSON...
hgm := HybridGroupMessage{
MemberGroupID: uint32(mgid),
MemberMessageID: uint32(mmid),
Sent: uint64(time.Now().UnixMilli()),
Author: profile.GetOnion(),
MessageBody: message,
Signature: []byte{}, // Leave blank so we can sign this message...
}
data, err := json.Marshal(hgm)
if err != nil {
return -1, err
}
// Don't forget to sign the message...
sig, err := profile.SignMessage(data)
if err != nil {
return -1, err
}
hgm.Signature = sig
ncm := NewClearMessageEvent{
HybridGroupMessage: hgm,
}
signedData, err := json.Marshal(ncm)
if err != nil {
return -1, err
}
mgm := ManageGroupEvent{
EventType: NewClearMessage,
Data: string(signedData),
}
odata, err := json.Marshal(mgm)
if err != nil {
return -1, err
}
overlay := model.MessageWrapper{
Overlay: model.OverlayManageGroupEvent,
Data: string(odata),
}
ojson, err := json.Marshal(overlay)
if err != nil {
return -1, err
}
// send the message to the manager and update our message is string for tracking...
_, err = profile.SendMessage(conversation, string(ojson))
if err != nil {
return -1, err
}
profile.SetConversationAttributeInt(conversation, mmidkey, mmid)
// ok there is still one more thing we need to do...
// insert this message as part of our group log, for members of the group
// this exists in channel 0 of the conversation with the group manager...
mgidstr := strconv.Itoa(mgid) // we need both MemberGroupId and MemberMessageId for attestation later on...
newmmidstr := strconv.Itoa(mmid)
attr := model.Attributes{MemberGroupIDKey: mgidstr, MemberMessageIDKey: newmmidstr, constants.AttrAuthor: profile.GetOnion(), constants.AttrAck: event.False, constants.AttrSentTimestamp: time.Now().Format(time.RFC3339Nano)}
return profile.InternalInsertMessage(conversation, 0, hgm.Author, message, attr, hgm.Signature)
}
func (f ManagedGroupFunctionality) OnContactRequestValue(profile peer.CwtchPeer, conversation model.Conversation, eventID string, path attr.ScopedZonedPath) {
// nop hybrid group conversations do not exchange contact requests
}
func (f ManagedGroupFunctionality) OnContactReceiveValue(profile peer.CwtchPeer, conversation model.Conversation, path attr.ScopedZonedPath, value string, exists bool) {
// nop hybrid group conversations do not exchange contact requests
}

View File

@ -0,0 +1,75 @@
package inter
import (
"errors"
"strings"
"cwtch.im/cwtch/functionality/hybrid"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
)
// This functionality is a little different. It's not functionality per-se. It's a wrapper around
// CwtchProfile function that combines some core-functionalities like Hybrid Groups so that
// they can be transparently exposed in autobindings.
// DEV NOTE: consider moving other cross-cutting interface functions here to simplfy CwtchPeer
type InterfaceFunctionality struct {
}
// FunctionalityGate returns filesharing functionality - gates now happen on function calls.
func FunctionalityGate() *InterfaceFunctionality {
return new(InterfaceFunctionality)
}
func (i InterfaceFunctionality) ImportBundle(profile peer.CwtchPeer, uri string) error {
// check if this is a managed group. Note: managed groups do not comply with the server bundle format.
if strings.HasPrefix(uri, "managed:") {
uri = uri[len("managed:"):]
if profile.IsFeatureEnabled(constants.GroupsExperiment) {
mgf := hybrid.ManagedGroupFunctionality{}
return mgf.NewManagedGroup(profile, uri)
} else {
return errors.New("managed groups require the group experiment to be enabled")
}
}
// DEV NOTE: we may want to eventually move Server Import code to ServerFunctionality and add a hook here...
// DEV NOTE: consider making ImportBundle a high-level functionality interface? to support different kinds of contacts?
return profile.ImportBundle(uri)
}
// EnhancedImportBundle is identical to EnhancedImportBundle in CwtchPeer but instead of wrapping CwtchPeer.ImportBundle it instead
// wraps InterfaceFunctionality.ImportBundle
func (i InterfaceFunctionality) EnhancedImportBundle(profile peer.CwtchPeer, uri string) string {
err := i.ImportBundle(profile, uri)
if err == nil {
return "importBundle.success"
}
return err.Error()
}
// SendMessage sends a message to a conversation.
// NOTE: Unlike CwtchPeer.SendMessage this interface makes no guarentees about the raw-ness of the message sent to peer contacts.
// If the conversation is a hybrid groups then the message may be wrapped in multiple layers of overlay messages / encryption
// prior to being send. To send a raw message to a peer then use peer.CwtchPeer
// DEV NOTE: Move Legacy Group message send here...
func (i InterfaceFunctionality) SendMessage(profile peer.CwtchPeer, conversation int, message string) (int, error) {
ci, err := profile.GetConversationInfo(conversation)
if err != nil {
return -1, err
}
if ci.ACL[ci.Handle].ManageGroup {
mgf := hybrid.ManagedGroupFunctionality{}
return mgf.SendMessageToManagedGroup(profile, conversation, message)
}
return profile.SendMessage(conversation, message)
}
// EnhancedSendMessage Attempts to Send a Message and Immediately Attempts to Lookup the Message in the Database
// this wraps InterfaceFunctionality.SendMessage to support HybridGroups
func (i InterfaceFunctionality) EnhancedSendMessage(profile peer.CwtchPeer, conversation int, message string) string {
mid, err := i.SendMessage(profile, conversation, message)
if err != nil {
return ""
}
return profile.EnhancedGetMessageById(conversation, mid)
}

1
go.mod
View File

@ -16,7 +16,6 @@ require (
require (
filippo.io/edwards25519 v1.0.0 // indirect
git.openprivacy.ca/openprivacy/bine v0.0.5 // indirect
github.com/client9/misspell v0.3.4 // indirect
github.com/google/go-cmp v0.5.8 // indirect
github.com/gtank/merlin v0.1.1 // indirect
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect

2
go.sum
View File

@ -8,8 +8,6 @@ git.openprivacy.ca/openprivacy/connectivity v1.11.0 h1:roASjaFtQLu+HdH5fa2wx6F00
git.openprivacy.ca/openprivacy/connectivity v1.11.0/go.mod h1:OQO1+7OIz/jLxDrorEMzvZA6SEbpbDyLGpjoFqT3z1Y=
git.openprivacy.ca/openprivacy/log v1.0.3 h1:E/PMm4LY+Q9s3aDpfySfEDq/vYQontlvNj/scrPaga0=
git.openprivacy.ca/openprivacy/log v1.0.3/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

View File

@ -20,6 +20,9 @@ const (
// LegacyGroupZone for attributes related to legacy group experiment
LegacyGroupZone = Zone("legacygroup")
// ConversationZone for attributes related to structure of the conversation
ConversationZone = Zone("conversation")
// FilesharingZone for attributes related to file sharing
FilesharingZone = Zone("filesharing")
@ -65,6 +68,8 @@ func ParseZone(path string) (Zone, string) {
return ServerKeyZone, parts[1]
case ServerZone:
return ServerZone, parts[1]
case ConversationZone:
return ConversationZone, parts[1]
default:
return UnknownZone, parts[1]
}

View File

@ -58,6 +58,7 @@ const SyncMostRecentMessageTime = "SyncMostRecentMessageTime"
const AttrLastConnectionTime = "last-connection-time"
const PeerAutostart = "autostart"
const PeerAppearOffline = "appear-offline"
const PrivateName = "private-name"
const Archived = "archived"
const ProfileStatus = "profile-status"
@ -71,3 +72,4 @@ const Description = "description"
// Used to store the status of acl migrations
const ACLVersion = "acl-version"
const ACLVersionOne = "acl-v1"
const ACLVersionTwo = "acl-v2"

View File

@ -0,0 +1,4 @@
package constants
const CHANNEL_CHAT = 0
const CHANNEL_MANAGER = 2

3