Fixed a few bugs and build libSession-util from source
Added libSession-util as a submodule and wired into build Updated the logic to run migrations when returning from the background as well (since we will have feature-flag controlled migrations it's possible for a "new" migration to become available at this point) Fixed an issue where the 'Note to Self' conversation could appear when linking a device for a new user Fixed an issue where the app would process the ConfigSyncJob before completing onboarding
This commit is contained in:
parent
ad3e53d235
commit
6fd574916b
|
@ -0,0 +1,3 @@
|
|||
[submodule "LibSession-Util"]
|
||||
path = LibSession-Util
|
||||
url = git@github.com:oxen-io/libsession-util.git
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 53c824de0d514307f3bad6a62449166bd10da6f8
|
|
@ -0,0 +1,83 @@
|
|||
#!/bin/bash
|
||||
|
||||
# XCode will error during it's dependency graph construction (which happens before the build
|
||||
# stage starts and any target "Run Script" phases are triggered)
|
||||
#
|
||||
# In order to avoid this error we need to build the framework before actually getting to the
|
||||
# build stage so XCode is able to build the dependency graph
|
||||
#
|
||||
# XCode's Pre-action scripts don't output anything into XCode so the only way to emit a useful
|
||||
# error is to return a success status and have the project detect and log the error itself then
|
||||
# log it, stopping the build at that point
|
||||
#
|
||||
# The other step to get this to work properly is to ensure the framework in "Link Binary with
|
||||
# Libraries" isn't using a relative directory, unfortunately there doesn't seem to be a good
|
||||
# way to do this directly so we need to modify the '.pbxproj' file directly, updating the
|
||||
# framework entry to have the following (on a single line):
|
||||
# {
|
||||
# isa = PBXFileReference;
|
||||
# explicitFileType = wrapper.xcframework;
|
||||
# includeInIndex = 0;
|
||||
# path = "{FRAMEWORK NAME GOES HERE}";
|
||||
# sourceTree = BUILD_DIR;
|
||||
# };
|
||||
|
||||
# Need to set the path or we won't find cmake
|
||||
PATH=${PATH}:/usr/local/bin:/opt/homebrew/bin:/sbin/md5
|
||||
|
||||
# Direct the output to a log file
|
||||
exec > "${TARGET_BUILD_DIR}/libsession_util_output.log" 2>&1
|
||||
|
||||
# Remove any old build errors
|
||||
rm -rf "${TARGET_BUILD_DIR}/libsession_util_error.log"
|
||||
|
||||
# First ensure cmake is installed (store the error in a log and exit with a success status - xcode will output the error)
|
||||
if ! which cmake > /dev/null; then
|
||||
echo "error: cmake is required to build, please install (can install via homebrew with 'brew install cmake')." > "${TARGET_BUILD_DIR}/error.log"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Generate a hash of the libSession-util source files and check if they differ from the last hash
|
||||
NEW_SOURCE_HASH=$(find "${SRCROOT}/LibSession-Util/src" -type f -exec md5 {} + | awk '{print $NF}' | sort | md5 | awk '{print $NF}')
|
||||
NEW_HEADER_HASH=$(find "${SRCROOT}/LibSession-Util/include" -type f -exec md5 {} + | awk '{print $NF}' | sort | md5 | awk '{print $NF}')
|
||||
|
||||
if [ -f "${TARGET_BUILD_DIR}/libsession_util_source_hash.log" ]; then
|
||||
read -r OLD_SOURCE_HASH < "${TARGET_BUILD_DIR}/libsession_util_source_hash.log"
|
||||
fi
|
||||
|
||||
if [ -f "${TARGET_BUILD_DIR}/libsession_util_header_hash.log" ]; then
|
||||
read -r OLD_HEADER_HASH < "${TARGET_BUILD_DIR}/libsession_util_header_hash.log"
|
||||
fi
|
||||
|
||||
if [ -f "${TARGET_BUILD_DIR}/libsession_util_archs.log" ]; then
|
||||
read -r OLD_ARCHS < "${TARGET_BUILD_DIR}/libsession_util_archs.log"
|
||||
fi
|
||||
|
||||
# Start the libSession-util build if it doesn't already exists
|
||||
if [ "${NEW_SOURCE_HASH}" != "${OLD_SOURCE_HASH}" ] || [ "${NEW_HEADER_HASH}" != "${OLD_HEADER_HASH}" ] || [ "${ARCHS[*]}" != "${OLD_ARCHS}" ] || [ ! -d "${TARGET_BUILD_DIR}/libsession-util.xcframework" ]; then
|
||||
echo "info: Build is not up-to-date - creating new build"
|
||||
echo ""
|
||||
|
||||
# Remove any existing build files (just to be safe)
|
||||
rm -rf "${TARGET_BUILD_DIR}/libsession-util.a"
|
||||
rm -rf "${TARGET_BUILD_DIR}/libsession-util.xcframework"
|
||||
rm -rf "${BUILD_DIR}/libsession-util.xcframework"
|
||||
|
||||
# Trigger the new build
|
||||
cd "${SRCROOT}/LibSession-Util"
|
||||
result=$(./utils/ios.sh "libsession-util" false)
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "error: Failed to build libsession-util (See details in '${TARGET_BUILD_DIR}/pre-action-output.log')." > "${TARGET_BUILD_DIR}/error.log"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Save the updated source hash to disk to prevent rebuilds when there were no changes
|
||||
echo "${NEW_SOURCE_HASH}" > "${TARGET_BUILD_DIR}/libsession_util_source_hash.log"
|
||||
echo "${NEW_HEADER_HASH}" > "${TARGET_BUILD_DIR}/libsession_util_header_hash.log"
|
||||
echo "${ARCHS[*]}" > "${TARGET_BUILD_DIR}/libsession_util_archs.log"
|
||||
fi
|
||||
|
||||
# Move the target-specific libSession-util build to the parent build directory (so XCode can have a reference to a single build)
|
||||
rm -rf "${BUILD_DIR}/libsession-util.xcframework"
|
||||
cp -r "${TARGET_BUILD_DIR}/libsession-util.xcframework" "${BUILD_DIR}/libsession-util.xcframework"
|
|
@ -3,7 +3,7 @@
|
|||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 52;
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
|
@ -436,7 +436,6 @@
|
|||
C3C2A5A7255385C100C340D1 /* SessionSnodeKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
C3C2A5C0255385EE00C340D1 /* Snode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5B7255385EC00C340D1 /* Snode.swift */; };
|
||||
C3C2A5C2255385EE00C340D1 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5B9255385ED00C340D1 /* Configuration.swift */; };
|
||||
C3C2A5DE2553860B00C340D1 /* String+Trimming.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D22553860900C340D1 /* String+Trimming.swift */; };
|
||||
C3C2A5E02553860B00C340D1 /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D42553860A00C340D1 /* Threading.swift */; };
|
||||
C3C2A5E42553860B00C340D1 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */; };
|
||||
C3C2A67D255388CC00C340D1 /* SessionUtilitiesKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A67B255388CC00C340D1 /* SessionUtilitiesKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
|
@ -735,7 +734,6 @@
|
|||
FD87DCFE28B7582C00AF0F98 /* BlockedContactsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DCFD28B7582C00AF0F98 /* BlockedContactsViewModel.swift */; };
|
||||
FD87DD0028B820F200AF0F98 /* BlockedContactCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DCFF28B820F200AF0F98 /* BlockedContactCell.swift */; };
|
||||
FD87DD0428B8727D00AF0F98 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DD0328B8727D00AF0F98 /* Configuration.swift */; };
|
||||
FD8ECF7929340F7200C0D1BB /* libsession-util.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = FD8ECF7829340F7100C0D1BB /* libsession-util.xcframework */; };
|
||||
FD8ECF7B29340FFD00C0D1BB /* SessionUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7A29340FFD00C0D1BB /* SessionUtil.swift */; };
|
||||
FD8ECF7D2934293A00C0D1BB /* _013_SessionUtilChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7C2934293A00C0D1BB /* _013_SessionUtilChanges.swift */; };
|
||||
FD8ECF7F2934298100C0D1BB /* ConfigDump.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7E2934298100C0D1BB /* ConfigDump.swift */; };
|
||||
|
@ -906,12 +904,15 @@
|
|||
FDF848F329413DB0007DCAE5 /* ImagePickerHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848F229413DB0007DCAE5 /* ImagePickerHandler.swift */; };
|
||||
FDF848F529413EEC007DCAE5 /* SessionCell+Styling.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848F429413EEC007DCAE5 /* SessionCell+Styling.swift */; };
|
||||
FDF848F729414477007DCAE5 /* CurrentUserPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848F629414477007DCAE5 /* CurrentUserPoller.swift */; };
|
||||
FDFC4D9A29F0C51500992FB6 /* String+Trimming.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D22553860900C340D1 /* String+Trimming.swift */; };
|
||||
FDFC4E1929F1F9A600992FB6 /* libsession-util.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = FDFC4E1829F1F9A600992FB6 /* libsession-util.xcframework */; };
|
||||
FDFD645927F26C6800808CA1 /* Array+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D12553860800C340D1 /* Array+Utilities.swift */; };
|
||||
FDFD645D27F273F300808CA1 /* MockGeneralCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */; };
|
||||
FDFDE124282D04F20098B17F /* MediaDismissAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFDE123282D04F20098B17F /* MediaDismissAnimationController.swift */; };
|
||||
FDFDE126282D05380098B17F /* MediaInteractiveDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFDE125282D05380098B17F /* MediaInteractiveDismiss.swift */; };
|
||||
FDFDE128282D05530098B17F /* MediaPresentationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFDE127282D05530098B17F /* MediaPresentationContext.swift */; };
|
||||
FDFDE12A282D056B0098B17F /* MediaZoomAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFDE129282D056B0098B17F /* MediaZoomAnimationController.swift */; };
|
||||
FDFF61D729F2600300F95FB0 /* Identity+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFF61D629F2600300F95FB0 /* Identity+Utilities.swift */; };
|
||||
FE43694493EC2E1E438EBEB3 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit_SessionMessagingKitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 13D1714FDC4DAB121DA2C73A /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit_SessionMessagingKitTests.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
|
@ -923,13 +924,6 @@
|
|||
remoteGlobalIDString = 453518671FC635DD00210559;
|
||||
remoteInfo = SignalShareExtension;
|
||||
};
|
||||
7B251C3827D82D9E001A6284 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = D221A080169C9E5E00537ABF /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = C3C2A678255388CC00C340D1;
|
||||
remoteInfo = SessionUtilitiesKit;
|
||||
};
|
||||
7BC01A40241F40AB00BC7C55 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = D221A080169C9E5E00537ABF /* Project object */;
|
||||
|
@ -1233,7 +1227,6 @@
|
|||
7B2561C12978B307005C086C /* MediaInfoVC+MediaInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MediaInfoVC+MediaInfoView.swift"; sourceTree = "<group>"; };
|
||||
7B2561C329874851005C086C /* SessionCarouselView+Info.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCarouselView+Info.swift"; sourceTree = "<group>"; };
|
||||
7B2DB2AD26F1B0FF0035B509 /* si */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = si; path = si.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
7B2E985729AC227C001792D7 /* UIContextualAction+Theming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Theming.swift"; sourceTree = "<group>"; };
|
||||
7B3A392D2977791E002FE4AC /* MediaInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaInfoVC.swift; sourceTree = "<group>"; };
|
||||
7B3A392F297A3919002FE4AC /* MediaInfoVC+MediaPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MediaInfoVC+MediaPreviewView.swift"; sourceTree = "<group>"; };
|
||||
7B3A39312980D02B002FE4AC /* SessionCarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCarouselView.swift; sourceTree = "<group>"; };
|
||||
|
@ -1252,7 +1245,6 @@
|
|||
7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _007_HomeQueryOptimisationIndexes.swift; sourceTree = "<group>"; };
|
||||
7B81682928B6F1420069F315 /* ReactionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionResponse.swift; sourceTree = "<group>"; };
|
||||
7B81682B28B72F480069F315 /* PendingChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingChange.swift; sourceTree = "<group>"; };
|
||||
7B89FF4529C016E300C4C708 /* _012_AddFTSIfNeeded.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _012_AddFTSIfNeeded.swift; sourceTree = "<group>"; };
|
||||
7B8C44C428B49DDA00FBE25F /* NewConversationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewConversationVC.swift; sourceTree = "<group>"; };
|
||||
7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleMessage+Reaction.swift"; sourceTree = "<group>"; };
|
||||
7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -1877,7 +1869,6 @@
|
|||
FD87DCFD28B7582C00AF0F98 /* BlockedContactsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedContactsViewModel.swift; sourceTree = "<group>"; };
|
||||
FD87DCFF28B820F200AF0F98 /* BlockedContactCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedContactCell.swift; sourceTree = "<group>"; };
|
||||
FD87DD0328B8727D00AF0F98 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
|
||||
FD8ECF7829340F7100C0D1BB /* libsession-util.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = "libsession-util.xcframework"; sourceTree = "<group>"; };
|
||||
FD8ECF7A29340FFD00C0D1BB /* SessionUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionUtil.swift; sourceTree = "<group>"; };
|
||||
FD8ECF7C2934293A00C0D1BB /* _013_SessionUtilChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _013_SessionUtilChanges.swift; sourceTree = "<group>"; };
|
||||
FD8ECF7E2934298100C0D1BB /* ConfigDump.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigDump.swift; sourceTree = "<group>"; };
|
||||
|
@ -2048,12 +2039,14 @@
|
|||
FDF848F229413DB0007DCAE5 /* ImagePickerHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePickerHandler.swift; sourceTree = "<group>"; };
|
||||
FDF848F429413EEC007DCAE5 /* SessionCell+Styling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SessionCell+Styling.swift"; sourceTree = "<group>"; };
|
||||
FDF848F629414477007DCAE5 /* CurrentUserPoller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrentUserPoller.swift; sourceTree = "<group>"; };
|
||||
FDFC4E1829F1F9A600992FB6 /* libsession-util.xcframework */ = {isa = PBXFileReference; explicitFileType = wrapper.xcframework; includeInIndex = 0; path = "libsession-util.xcframework"; sourceTree = BUILD_DIR; };
|
||||
FDFD645A27F26D4600808CA1 /* Data+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = "<group>"; };
|
||||
FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockGeneralCache.swift; sourceTree = "<group>"; };
|
||||
FDFDE123282D04F20098B17F /* MediaDismissAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaDismissAnimationController.swift; sourceTree = "<group>"; };
|
||||
FDFDE125282D05380098B17F /* MediaInteractiveDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaInteractiveDismiss.swift; sourceTree = "<group>"; };
|
||||
FDFDE127282D05530098B17F /* MediaPresentationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPresentationContext.swift; sourceTree = "<group>"; };
|
||||
FDFDE129282D056B0098B17F /* MediaZoomAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaZoomAnimationController.swift; sourceTree = "<group>"; };
|
||||
FDFF61D629F2600300F95FB0 /* Identity+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Identity+Utilities.swift"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -2124,7 +2117,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
FD8ECF7929340F7200C0D1BB /* libsession-util.xcframework in Frameworks */,
|
||||
FDFC4E1929F1F9A600992FB6 /* libsession-util.xcframework in Frameworks */,
|
||||
FDC4386C27B4E90300C60D73 /* SessionUtilitiesKit.framework in Frameworks */,
|
||||
C3C2A70B25539E1E00C340D1 /* SessionSnodeKit.framework in Frameworks */,
|
||||
BE25D9230CA2C3A40A9216EF /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit.framework in Frameworks */,
|
||||
|
@ -3265,6 +3258,7 @@
|
|||
FD772899284AF1BD0018502F /* Sodium+Utilities.swift */,
|
||||
C3A3A170256E1D25004D228D /* SSKReachabilityManager.swift */,
|
||||
C3ECBF7A257056B700EA7FCE /* Threading.swift */,
|
||||
FDFF61D629F2600300F95FB0 /* Identity+Utilities.swift */,
|
||||
);
|
||||
path = Utilities;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3343,9 +3337,9 @@
|
|||
C352A2F325574B3300338F3E /* Jobs */,
|
||||
C3A7215C2558C0AC0043A11F /* File Server */,
|
||||
C3A721332558BDDF0043A11F /* Open Groups */,
|
||||
FD8ECF7529340F4800C0D1BB /* SessionUtil */,
|
||||
FD3E0C82283B581F002A425C /* Shared Models */,
|
||||
C3BBE0B32554F0D30050F1E3 /* Utilities */,
|
||||
FD8ECF7529340F4800C0D1BB /* LibSessionUtil */,
|
||||
FDC438C027BB4E6800C60D73 /* SMKDependencies.swift */,
|
||||
FD245C612850664300B966DD /* Configuration.swift */,
|
||||
);
|
||||
|
@ -3502,7 +3496,6 @@
|
|||
D221A08C169C9E5E00537ABF /* Frameworks */,
|
||||
D221A08A169C9E5E00537ABF /* Products */,
|
||||
2BADBA206E0B8D297E313FBA /* Pods */,
|
||||
FD368A6629DE86A9000DBF1E /* Recovered References */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
|
@ -3527,6 +3520,7 @@
|
|||
D221A08C169C9E5E00537ABF /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FDFC4E1829F1F9A600992FB6 /* libsession-util.xcframework */,
|
||||
B8DE1FAF26C228780079C9CE /* SignalRingRTC.framework */,
|
||||
C35E8AA22485C72300ACB629 /* SwiftCSV.framework */,
|
||||
B847570023D568EB00759540 /* SignalServiceKit.framework */,
|
||||
|
@ -3794,15 +3788,6 @@
|
|||
path = Database;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD368A6629DE86A9000DBF1E /* Recovered References */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7B2E985729AC227C001792D7 /* UIContextualAction+Theming.swift */,
|
||||
7B89FF4529C016E300C4C708 /* _012_AddFTSIfNeeded.swift */,
|
||||
);
|
||||
name = "Recovered References";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD37E9C428A1C701003AE748 /* Themes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -4095,17 +4080,16 @@
|
|||
path = Types;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD8ECF7529340F4800C0D1BB /* LibSessionUtil */ = {
|
||||
FD8ECF7529340F4800C0D1BB /* SessionUtil */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FD2B4B022949886900AB4848 /* Database */,
|
||||
FD8ECF8E29381FB200C0D1BB /* Config Handling */,
|
||||
FD432435299DEA1C008A0213 /* Utilities */,
|
||||
FD8ECF7829340F7100C0D1BB /* libsession-util.xcframework */,
|
||||
FD8ECF882935AB7200C0D1BB /* SessionUtilError.swift */,
|
||||
FD8ECF7A29340FFD00C0D1BB /* SessionUtil.swift */,
|
||||
);
|
||||
path = LibSessionUtil;
|
||||
path = SessionUtil;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD8ECF802934385900C0D1BB /* LibSessionUtil */ = {
|
||||
|
@ -4122,11 +4106,11 @@
|
|||
FD8ECF8E29381FB200C0D1BB /* Config Handling */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FD8ECF8F29381FC200C0D1BB /* SessionUtil+UserProfile.swift */,
|
||||
FD2B4AFC294688D000AB4848 /* SessionUtil+Contacts.swift */,
|
||||
FD43EE9E297E2EE0009C87C5 /* SessionUtil+ConvoInfoVolatile.swift */,
|
||||
FD43EE9C297A5190009C87C5 /* SessionUtil+UserGroups.swift */,
|
||||
FDA1E83A29A5F2D500C5C3BD /* SessionUtil+Shared.swift */,
|
||||
FD43EE9C297A5190009C87C5 /* SessionUtil+UserGroups.swift */,
|
||||
FD8ECF8F29381FC200C0D1BB /* SessionUtil+UserProfile.swift */,
|
||||
);
|
||||
path = "Config Handling";
|
||||
sourceTree = "<group>";
|
||||
|
@ -4620,6 +4604,7 @@
|
|||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = C3C2A6F925539DE700C340D1 /* Build configuration list for PBXNativeTarget "SessionMessagingKit" */;
|
||||
buildPhases = (
|
||||
FDFC4E1729F14F7A00992FB6 /* Validate pre-build actions */,
|
||||
2014435DF351DF6C60122751 /* [CP] Check Pods Manifest.lock */,
|
||||
C3C2A6EB25539DE700C340D1 /* Headers */,
|
||||
C3C2A6EC25539DE700C340D1 /* Sources */,
|
||||
|
@ -4629,7 +4614,6 @@
|
|||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
7B251C3927D82D9E001A6284 /* PBXTargetDependency */,
|
||||
);
|
||||
name = SessionMessagingKit;
|
||||
productName = SessionMessagingKit;
|
||||
|
@ -4734,7 +4718,7 @@
|
|||
isa = PBXProject;
|
||||
attributes = {
|
||||
DefaultBuildSystemTypeForWorkspace = Original;
|
||||
LastSwiftUpdateCheck = 1340;
|
||||
LastSwiftUpdateCheck = 1430;
|
||||
LastTestingUpgradeCheck = 0600;
|
||||
LastUpgradeCheck = 1400;
|
||||
ORGANIZATIONNAME = "Rangeproof Pty Ltd";
|
||||
|
@ -5340,6 +5324,7 @@
|
|||
};
|
||||
FDE7214D287E50820093DF33 /* Lint Localizable.strings */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
|
@ -5357,6 +5342,26 @@
|
|||
shellScript = "\"${SRCROOT}/Scripts/LintLocalizableStrings.swift\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
FDFC4E1729F14F7A00992FB6 /* Validate pre-build actions */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Validate pre-build actions";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if [ -f \"${TARGET_BUILD_DIR}/libsession_util_error.log\" ]; then\n read -r line < \"${TARGET_BUILD_DIR}/libsession_util_error.log\"\n echo \"${line}\"\n exit 1\nfi\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
|
@ -5534,8 +5539,8 @@
|
|||
FDF848BC29405C5A007DCAE5 /* SnodeRecursiveResponse.swift in Sources */,
|
||||
FDF848C029405C5A007DCAE5 /* ONSResolveResponse.swift in Sources */,
|
||||
FD17D7A427F40F8100122BE0 /* _003_YDBToGRDBMigration.swift in Sources */,
|
||||
FDFC4D9A29F0C51500992FB6 /* String+Trimming.swift in Sources */,
|
||||
FDF848C629405C5B007DCAE5 /* DeleteAllMessagesRequest.swift in Sources */,
|
||||
C3C2A5DE2553860B00C340D1 /* String+Trimming.swift in Sources */,
|
||||
FDF848D429405C5B007DCAE5 /* DeleteAllBeforeResponse.swift in Sources */,
|
||||
FDF848D629405C5B007DCAE5 /* SnodeMessage.swift in Sources */,
|
||||
FDF848D129405C5B007DCAE5 /* SnodeSwarmItem.swift in Sources */,
|
||||
|
@ -5551,7 +5556,6 @@
|
|||
FDF848D229405C5B007DCAE5 /* LegacyGetMessagesRequest.swift in Sources */,
|
||||
FDF848CB29405C5B007DCAE5 /* SnodePoolResponse.swift in Sources */,
|
||||
FDF848C429405C5A007DCAE5 /* RevokeSubkeyResponse.swift in Sources */,
|
||||
C3C2A5DE2553860B00C340D1 /* String+Trimming.swift in Sources */,
|
||||
FD26FA6D291DADAE005801D8 /* (null) in Sources */,
|
||||
FDF848E529405D6E007DCAE5 /* SnodeAPIError.swift in Sources */,
|
||||
FDF848D529405C5B007DCAE5 /* DeleteAllMessagesResponse.swift in Sources */,
|
||||
|
@ -5740,6 +5744,7 @@
|
|||
FD716E6628502EE200C96BF4 /* CurrentCallProtocol.swift in Sources */,
|
||||
FD8ECF9029381FC200C0D1BB /* SessionUtil+UserProfile.swift in Sources */,
|
||||
FD09B7E5288670BB00ED0B66 /* _008_EmojiReacts.swift in Sources */,
|
||||
FDFF61D729F2600300F95FB0 /* Identity+Utilities.swift in Sources */,
|
||||
FDC4385F27B4C4A200C60D73 /* PinnedMessage.swift in Sources */,
|
||||
7BFD1A8C2747150E00FB91B9 /* TurnServerInfo.swift in Sources */,
|
||||
B8BF43BA26CC95FB007828D1 /* WebRTC+Utilities.swift in Sources */,
|
||||
|
@ -6188,11 +6193,6 @@
|
|||
target = 453518671FC635DD00210559 /* SessionShareExtension */;
|
||||
targetProxy = 453518701FC635DD00210559 /* PBXContainerItemProxy */;
|
||||
};
|
||||
7B251C3927D82D9E001A6284 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */;
|
||||
targetProxy = 7B251C3827D82D9E001A6284 /* PBXContainerItemProxy */;
|
||||
};
|
||||
7BC01A41241F40AB00BC7C55 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 7BC01A3A241F40AB00BC7C55 /* SessionNotificationServiceExtension */;
|
||||
|
@ -7350,7 +7350,6 @@
|
|||
ENABLE_BITCODE = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = "\"$(SRCROOT)/SessionMessagingKit/LibSessionUtil\"";
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
|
@ -7391,7 +7390,6 @@
|
|||
);
|
||||
"OTHER_SWIFT_FLAGS[arch=*]" = "-D DEBUG";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_INCLUDE_PATHS = "\"$(SRCROOT)/SessionMessagingKit/LibSessionUtil\"";
|
||||
SWIFT_VERSION = 4.0;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
|
@ -7427,7 +7425,6 @@
|
|||
CODE_SIGN_IDENTITY = "iPhone Distribution";
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = "\"$(SRCROOT)/SessionMessagingKit/LibSessionUtil\"";
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES;
|
||||
GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES;
|
||||
|
@ -7465,7 +7462,6 @@
|
|||
);
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_INCLUDE_PATHS = "\"$(SRCROOT)/SessionMessagingKit/LibSessionUtil\"";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 4.0;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
|
|
|
@ -5,6 +5,24 @@
|
|||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Build libSession"
|
||||
scriptText = ""${SRCROOT}/Scripts/build_libSession_util.sh" ">
|
||||
<EnvironmentBuildable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D221A088169C9E5E00537ABF"
|
||||
BuildableName = "Session.app"
|
||||
BlueprintName = "Session"
|
||||
ReferencedContainer = "container:Session.xcodeproj">
|
||||
</BuildableReference>
|
||||
</EnvironmentBuildable>
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
|
|
|
@ -1,10 +1,28 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1400"
|
||||
version = "1.3">
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Build libSession"
|
||||
scriptText = ""${SRCROOT}/Scripts/build_libSession_util.sh" ">
|
||||
<EnvironmentBuildable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C3C2A6EF25539DE700C340D1"
|
||||
BuildableName = "SessionMessagingKit.framework"
|
||||
BlueprintName = "SessionMessagingKit"
|
||||
ReferencedContainer = "container:Session.xcodeproj">
|
||||
</BuildableReference>
|
||||
</EnvironmentBuildable>
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
|
|
|
@ -6,6 +6,24 @@
|
|||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Build libSession"
|
||||
scriptText = ""${SRCROOT}/Scripts/build_libSession_util.sh" ">
|
||||
<EnvironmentBuildable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "7BC01A3A241F40AB00BC7C55"
|
||||
BuildableName = "SessionNotificationServiceExtension.appex"
|
||||
BlueprintName = "SessionNotificationServiceExtension"
|
||||
ReferencedContainer = "container:Session.xcodeproj">
|
||||
</BuildableReference>
|
||||
</EnvironmentBuildable>
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
|
|
|
@ -6,6 +6,24 @@
|
|||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Build libSession"
|
||||
scriptText = ""${SRCROOT}/Scripts/build_libSession_util.sh" ">
|
||||
<EnvironmentBuildable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "453518671FC635DD00210559"
|
||||
BuildableName = "SessionShareExtension.appex"
|
||||
BlueprintName = "SessionShareExtension"
|
||||
ReferencedContainer = "container:Session.xcodeproj">
|
||||
</BuildableReference>
|
||||
</EnvironmentBuildable>
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
|
|
|
@ -1,10 +1,28 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1400"
|
||||
version = "1.3">
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Build libSession"
|
||||
scriptText = ""${SRCROOT}/Scripts/build_libSession_util.sh" ">
|
||||
<EnvironmentBuildable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C3C2A678255388CC00C340D1"
|
||||
BuildableName = "SessionUtilitiesKit.framework"
|
||||
BlueprintName = "SessionUtilitiesKit"
|
||||
ReferencedContainer = "container:Session.xcodeproj">
|
||||
</BuildableReference>
|
||||
</EnvironmentBuildable>
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
|
|
|
@ -1,10 +1,28 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1400"
|
||||
version = "1.3">
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Build libSession"
|
||||
scriptText = ""${SRCROOT}/Scripts/build_libSession_util.sh" ">
|
||||
<EnvironmentBuildable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C33FD9AA255A548A00E217F9"
|
||||
BuildableName = "SignalUtilitiesKit.framework"
|
||||
BlueprintName = "SignalUtilitiesKit"
|
||||
ReferencedContainer = "container:Session.xcodeproj">
|
||||
</BuildableReference>
|
||||
</EnvironmentBuildable>
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
|
|
|
@ -282,7 +282,8 @@ final class HomeVC: BaseVC, SessionUtilRespondingViewController, UITableViewData
|
|||
if Identity.userExists(), let appDelegate: AppDelegate = UIApplication.shared.delegate as? AppDelegate {
|
||||
appDelegate.startPollersIfNeeded()
|
||||
|
||||
if !Features.useSharedUtilForUserConfig {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
if !SessionUtil.userConfigsEnabled {
|
||||
// Do this only if we created a new Session ID, or if we already received the initial configuration message
|
||||
if UserDefaults.standard[.hasSyncedInitialConfiguration] {
|
||||
appDelegate.syncConfigurationIfNeeded()
|
||||
|
|
|
@ -19,6 +19,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
var hasInitialRootViewController: Bool = false
|
||||
private var loadingViewController: LoadingViewController?
|
||||
|
||||
enum LifecycleMethod {
|
||||
case finishLaunching
|
||||
case enterForeground
|
||||
}
|
||||
|
||||
/// This needs to be a lazy variable to ensure it doesn't get initialized before it actually needs to be used
|
||||
lazy var poller: CurrentUserPoller = CurrentUserPoller()
|
||||
|
||||
|
@ -69,11 +74,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
},
|
||||
migrationsCompletion: { [weak self] result, needsConfigSync in
|
||||
if case .failure(let error) = result {
|
||||
self?.showFailedMigrationAlert(error: error)
|
||||
self?.showFailedMigrationAlert(calledFrom: .finishLaunching, error: error)
|
||||
return
|
||||
}
|
||||
|
||||
self?.completePostMigrationSetup(needsConfigSync: needsConfigSync)
|
||||
self?.completePostMigrationSetup(calledFrom: .finishLaunching, needsConfigSync: needsConfigSync)
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -126,6 +131,28 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
|
||||
// Resume database
|
||||
NotificationCenter.default.post(name: Database.resumeNotification, object: self)
|
||||
|
||||
// If we've already completed migrations at least once this launch then check
|
||||
// to see if any "delayed" migrations now need to run
|
||||
if Storage.shared.hasCompletedMigrations {
|
||||
AppReadiness.invalidate()
|
||||
AppSetup.runPostSetupMigrations(
|
||||
migrationProgressChanged: { [weak self] progress, minEstimatedTotalTime in
|
||||
self?.loadingViewController?.updateProgress(
|
||||
progress: progress,
|
||||
minEstimatedTotalTime: minEstimatedTotalTime
|
||||
)
|
||||
},
|
||||
migrationsCompletion: { [weak self] result, needsConfigSync in
|
||||
if case .failure(let error) = result {
|
||||
self?.showFailedMigrationAlert(calledFrom: .enterForeground, error: error)
|
||||
return
|
||||
}
|
||||
|
||||
self?.completePostMigrationSetup(calledFrom: .enterForeground, needsConfigSync: needsConfigSync)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
|
@ -250,7 +277,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
|
||||
// MARK: - App Readiness
|
||||
|
||||
private func completePostMigrationSetup(needsConfigSync: Bool) {
|
||||
private func completePostMigrationSetup(calledFrom lifecycleMethod: LifecycleMethod, needsConfigSync: Bool) {
|
||||
Configuration.performMainSetup()
|
||||
JobRunner.add(executor: SyncPushTokensJob.self, for: .syncPushTokens)
|
||||
|
||||
|
@ -268,7 +295,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
self.ensureRootViewController(isPreAppReadyCall: true)
|
||||
|
||||
// Trigger any launch-specific jobs and start the JobRunner
|
||||
JobRunner.appDidFinishLaunching()
|
||||
if lifecycleMethod == .finishLaunching {
|
||||
JobRunner.appDidFinishLaunching()
|
||||
}
|
||||
|
||||
// Note that this does much more than set a flag;
|
||||
// it will also run all deferred blocks (including the JobRunner
|
||||
|
@ -285,7 +314,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
// at least once in the post-SAE world.
|
||||
db[.isReadyForAppExtensions] = true
|
||||
|
||||
if Identity.userExists(db) {
|
||||
if Identity.userCompletedRequiredOnboarding(db) {
|
||||
let appVersion: AppVersion = AppVersion.sharedInstance()
|
||||
|
||||
// If the device needs to sync config or the user updated to a new version
|
||||
|
@ -301,7 +330,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
}
|
||||
}
|
||||
|
||||
private func showFailedMigrationAlert(error: Error?) {
|
||||
private func showFailedMigrationAlert(calledFrom lifecycleMethod: LifecycleMethod, error: Error?) {
|
||||
let alert = UIAlertController(
|
||||
title: "Session",
|
||||
message: "DATABASE_MIGRATION_FAILED".localized(),
|
||||
|
@ -309,7 +338,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
)
|
||||
alert.addAction(UIAlertAction(title: "HELP_REPORT_BUG_ACTION_TITLE".localized(), style: .default) { _ in
|
||||
HelpViewModel.shareLogs(viewControllerToDismiss: alert) { [weak self] in
|
||||
self?.showFailedMigrationAlert(error: error)
|
||||
self?.showFailedMigrationAlert(calledFrom: lifecycleMethod, error: error)
|
||||
}
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: "vc_restore_title".localized(), style: .destructive) { _ in
|
||||
|
@ -330,11 +359,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
},
|
||||
migrationsCompletion: { [weak self] result, needsConfigSync in
|
||||
if case .failure(let error) = result {
|
||||
self?.showFailedMigrationAlert(error: error)
|
||||
self?.showFailedMigrationAlert(calledFrom: lifecycleMethod, error: error)
|
||||
return
|
||||
}
|
||||
|
||||
self?.completePostMigrationSetup(needsConfigSync: needsConfigSync)
|
||||
self?.completePostMigrationSetup(calledFrom: lifecycleMethod, needsConfigSync: needsConfigSync)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
@ -497,7 +526,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
|
||||
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
|
||||
AppReadiness.runNowOrWhenAppDidBecomeReady {
|
||||
guard Identity.userExists() else { return }
|
||||
guard Identity.userCompletedRequiredOnboarding() else { return }
|
||||
|
||||
SessionApp.homeViewController.wrappedValue?.createNewConversation()
|
||||
completionHandler(true)
|
||||
|
@ -662,7 +691,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
|
||||
func syncConfigurationIfNeeded() {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard !Features.useSharedUtilForUserConfig else { return }
|
||||
guard !SessionUtil.userConfigsEnabled else { return }
|
||||
|
||||
let lastSync: Date = (UserDefaults.standard[.lastConfigurationSync] ?? .distantPast)
|
||||
|
||||
|
|
|
@ -24,12 +24,7 @@ public enum SyncPushTokensJob: JobExecutor {
|
|||
// Don't run when inactive or not in main app or if the user doesn't exist yet
|
||||
guard
|
||||
(UserDefaults.sharedLokiProject?[.isMainAppActive]).defaulting(to: false),
|
||||
Identity.userExists(),
|
||||
// If we have no display name then the user will be asked to enter one (this
|
||||
// can happen if the app crashed during onboarding which would leave the user
|
||||
// in an invalid state with no display name - the user is likely going to be
|
||||
// taken to the PN registration screen next which will re-trigger this job)
|
||||
!Profile.fetchOrCreateCurrentUser().name.isEmpty
|
||||
Identity.userCompletedRequiredOnboarding()
|
||||
else {
|
||||
deferred(job) // Don't need to do anything if it's not the main app
|
||||
return
|
||||
|
|
|
@ -10,7 +10,7 @@ import SessionMessagingKit
|
|||
enum Onboarding {
|
||||
private static let profileNameRetrievalPublisher: Atomic<AnyPublisher<String?, Error>> = {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard Features.useSharedUtilForUserConfig else {
|
||||
guard SessionUtil.userConfigsEnabled else {
|
||||
return Atomic(
|
||||
Just(nil)
|
||||
.setFailureType(to: Error.self)
|
||||
|
@ -39,7 +39,7 @@ enum Onboarding {
|
|||
)
|
||||
.tryFlatMap { receivedMessageTypes -> AnyPublisher<Void, Error> in
|
||||
// FIXME: Remove this entire 'tryFlatMap' once the updated user config has been released for long enough
|
||||
guard !receivedMessageTypes.isEmpty else {
|
||||
guard receivedMessageTypes.isEmpty else {
|
||||
return Just(())
|
||||
.setFailureType(to: Error.self)
|
||||
.eraseToAnyPublisher()
|
||||
|
@ -149,9 +149,19 @@ enum Onboarding {
|
|||
Contact.Columns.didApproveMe.set(to: true)
|
||||
)
|
||||
|
||||
// Create the 'Note to Self' thread (not visible by default)
|
||||
/// Create the 'Note to Self' thread (not visible by default)
|
||||
///
|
||||
/// **Note:** We need to explicitly `updateAllAndConfig` the `shouldBeVisible` value to `false`
|
||||
/// otherwise it won't actually get synced correctly
|
||||
try SessionThread
|
||||
.fetchOrCreate(db, id: x25519PublicKey, variant: .contact, shouldBeVisible: false)
|
||||
|
||||
try SessionThread
|
||||
.filter(id: x25519PublicKey)
|
||||
.updateAllAndConfig(
|
||||
db,
|
||||
SessionThread.Columns.shouldBeVisible.set(to: false)
|
||||
)
|
||||
}
|
||||
|
||||
// Set hasSyncedInitialConfiguration to true so that when we hit the
|
||||
|
|
|
@ -32,6 +32,7 @@ public enum SNMessagingKit { // Just to make the external API nice
|
|||
_013_SessionUtilChanges.self,
|
||||
// Wait until the feature is turned on before doing the migration that generates
|
||||
// the config dump data
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
(Features.useSharedUtilForUserConfig ?
|
||||
_014_GenerateInitialUserConfigDumps.self :
|
||||
(nil as Migration.Type?)
|
||||
|
|
|
@ -16,9 +16,12 @@ enum _014_GenerateInitialUserConfigDumps: Migration {
|
|||
|
||||
static func migrate(_ db: Database) throws {
|
||||
// If we have no ed25519 key then there is no need to create cached dump data
|
||||
guard let secretKey: [UInt8] = Identity.fetchUserEd25519KeyPair(db)?.secretKey else { return }
|
||||
guard let secretKey: [UInt8] = Identity.fetchUserEd25519KeyPair(db)?.secretKey else {
|
||||
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
|
||||
return
|
||||
}
|
||||
|
||||
// Load the initial config state if needed
|
||||
// Create the initial config state
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
|
||||
SessionUtil.loadState(db, userPublicKey: userPublicKey, ed25519SecretKey: secretKey)
|
||||
|
|
|
@ -229,25 +229,15 @@ public extension Profile {
|
|||
///
|
||||
/// **Note:** This method intentionally does **not** save the newly created Profile,
|
||||
/// it will need to be explicitly saved after calling
|
||||
static func fetchOrCreateCurrentUser() -> Profile {
|
||||
var userPublicKey: String = ""
|
||||
|
||||
let exisingProfile: Profile? = Storage.shared.read { db in
|
||||
userPublicKey = getUserHexEncodedPublicKey(db)
|
||||
|
||||
return try Profile.fetchOne(db, id: userPublicKey)
|
||||
}
|
||||
|
||||
return (exisingProfile ?? defaultFor(userPublicKey))
|
||||
}
|
||||
|
||||
/// Fetches or creates a Profile for the current user
|
||||
///
|
||||
/// **Note:** This method intentionally does **not** save the newly created Profile,
|
||||
/// it will need to be explicitly saved after calling
|
||||
static func fetchOrCreateCurrentUser(_ db: Database) -> Profile {
|
||||
static func fetchOrCreateCurrentUser(_ db: Database? = nil) -> Profile {
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
|
||||
guard let db: Database = db else {
|
||||
return Storage.shared
|
||||
.read { db in fetchOrCreateCurrentUser(db) }
|
||||
.defaulting(to: defaultFor(userPublicKey))
|
||||
}
|
||||
|
||||
return (
|
||||
(try? Profile.fetchOne(db, id: userPublicKey)) ??
|
||||
defaultFor(userPublicKey)
|
||||
|
|
|
@ -20,10 +20,10 @@ public enum ConfigurationSyncJob: JobExecutor {
|
|||
failure: @escaping (Job, Error?, Bool) -> (),
|
||||
deferred: @escaping (Job) -> ()
|
||||
) {
|
||||
guard Features.useSharedUtilForUserConfig else {
|
||||
success(job, true)
|
||||
return
|
||||
}
|
||||
guard
|
||||
SessionUtil.userConfigsEnabled,
|
||||
Identity.userCompletedRequiredOnboarding()
|
||||
else { return success(job, true) }
|
||||
|
||||
// On startup it's possible for multiple ConfigSyncJob's to run at the same time (which is
|
||||
// redundant) so check if there is another job already running and, if so, defer this job
|
||||
|
@ -175,14 +175,13 @@ public extension ConfigurationSyncJob {
|
|||
|
||||
static func enqueue(_ db: Database, publicKey: String) {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard Features.useSharedUtilForUserConfig else {
|
||||
guard SessionUtil.userConfigsEnabled else {
|
||||
// If we don't have a userKeyPair (or name) yet then there is no need to sync the
|
||||
// configuration as the user doesn't fully exist yet (this will get triggered on
|
||||
// the first launch of a fresh install due to the migrations getting run and a few
|
||||
// times during onboarding)
|
||||
guard
|
||||
Identity.userExists(db),
|
||||
!Profile.fetchOrCreateCurrentUser(db).name.isEmpty,
|
||||
Identity.userCompletedRequiredOnboarding(db),
|
||||
let legacyConfigMessage: Message = try? ConfigurationMessage.getCurrent(db)
|
||||
else { return }
|
||||
|
||||
|
@ -232,13 +231,13 @@ public extension ConfigurationSyncJob {
|
|||
|
||||
static func run() -> AnyPublisher<Void, Error> {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard Features.useSharedUtilForUserConfig else {
|
||||
guard SessionUtil.userConfigsEnabled else {
|
||||
return Storage.shared
|
||||
.writePublisher { db -> MessageSender.PreparedSendData in
|
||||
// If we don't have a userKeyPair yet then there is no need to sync the configuration
|
||||
// as the user doesn't exist yet (this will get triggered on the first launch of a
|
||||
// fresh install due to the migrations getting run)
|
||||
guard Identity.userExists(db) else { throw StorageError.generic }
|
||||
guard Identity.userCompletedRequiredOnboarding(db) else { throw StorageError.generic }
|
||||
|
||||
let publicKey: String = getUserHexEncodedPublicKey(db)
|
||||
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>AvailableLibraries</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>LibraryIdentifier</key>
|
||||
<string>ios-arm64_x86_64-simulator</string>
|
||||
<key>LibraryPath</key>
|
||||
<string>libsession-util.a</string>
|
||||
<key>SupportedArchitectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
<string>x86_64</string>
|
||||
</array>
|
||||
<key>SupportedPlatform</key>
|
||||
<string>ios</string>
|
||||
<key>SupportedPlatformVariant</key>
|
||||
<string>simulator</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>LibraryIdentifier</key>
|
||||
<string>ios-arm64</string>
|
||||
<key>LibraryPath</key>
|
||||
<string>libsession-util.a</string>
|
||||
<key>SupportedArchitectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>SupportedPlatform</key>
|
||||
<string>ios</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XFWK</string>
|
||||
<key>XCFrameworkFormatVersion</key>
|
||||
<string>1.0</string>
|
||||
</dict>
|
||||
</plist>
|
Binary file not shown.
Binary file not shown.
|
@ -1,21 +0,0 @@
|
|||
module SessionUtil {
|
||||
module capi {
|
||||
header "session/version.h"
|
||||
header "session/export.h"
|
||||
header "session/config.h"
|
||||
header "session/config/error.h"
|
||||
header "session/config/community.h"
|
||||
header "session/config/expiring.h"
|
||||
header "session/config/user_groups.h"
|
||||
header "session/config/convo_info_volatile.h"
|
||||
header "session/config/notify.h"
|
||||
header "session/config/user_profile.h"
|
||||
header "session/config/util.h"
|
||||
header "session/config/contacts.h"
|
||||
header "session/config/encrypt.h"
|
||||
header "session/config/base.h"
|
||||
header "session/config/profile_pic.h"
|
||||
header "session/xed25519.h"
|
||||
export *
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
#pragma once
|
||||
#include <oxenc/bt_value.h>
|
||||
|
||||
#include <cassert>
|
||||
#ifndef NDEBUG
|
||||
#include <algorithm>
|
||||
#endif
|
||||
|
||||
namespace session::bt {
|
||||
|
||||
using oxenc::bt_dict;
|
||||
using oxenc::bt_list;
|
||||
|
||||
/// Merges two bt dicts together: the returned dict includes all keys in a or b. Keys in *both*
|
||||
/// dicts get their value from `a`, otherwise the value is that of the dict that contains the key.
|
||||
bt_dict merge(const bt_dict& a, const bt_dict& b);
|
||||
|
||||
/// Merges two ordered bt_lists together using a predicate to determine order. The input lists must
|
||||
/// be sorted to begin with. `cmp` must be callable with a pair of `const bt_value&` arguments and
|
||||
/// must return true if the first argument should be considered less than the second argument. By
|
||||
/// default this skips elements from b that compare equal to a value of a, but you can include all
|
||||
/// the duplicates by specifying the `duplicates` parameter as true.
|
||||
template <typename Compare>
|
||||
bt_list merge_sorted(const bt_list& a, const bt_list& b, Compare cmp, bool duplicates = false) {
|
||||
bt_list result;
|
||||
auto it_a = a.begin();
|
||||
auto it_b = b.begin();
|
||||
|
||||
assert(std::is_sorted(it_a, a.end(), cmp));
|
||||
assert(std::is_sorted(it_b, b.end(), cmp));
|
||||
|
||||
if (duplicates) {
|
||||
while (it_a != a.end() && it_b != b.end()) {
|
||||
if (!cmp(*it_a, *it_b)) // *b <= *a
|
||||
result.push_back(*it_b++);
|
||||
else // *a < *b
|
||||
result.push_back(*it_a++);
|
||||
}
|
||||
} else {
|
||||
while (it_a != a.end() && it_b != b.end()) {
|
||||
if (cmp(*it_b, *it_a)) // *b < *a
|
||||
result.push_back(*it_b++);
|
||||
else if (cmp(*it_a, *it_b)) // *a < *b
|
||||
result.push_back(*it_a++);
|
||||
else // *a == *b
|
||||
++it_b; // skip it
|
||||
}
|
||||
}
|
||||
|
||||
if (it_a != a.end())
|
||||
result.insert(result.end(), it_a, a.end());
|
||||
else if (it_b != b.end())
|
||||
result.insert(result.end(), it_b, b.end());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace session::bt
|
|
@ -1,13 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef int64_t seqno_t;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
|
@ -1,352 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <oxenc/bt_serialize.h>
|
||||
#include <oxenc/bt_value.h>
|
||||
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include "types.hpp"
|
||||
|
||||
namespace session::config {
|
||||
|
||||
// FIXME: for multi-message we encode to longer and then split it up
|
||||
inline constexpr int MAX_MESSAGE_SIZE = 76800; // 76.8kB = Storage server's limit
|
||||
|
||||
// Application data data types:
|
||||
using scalar = std::variant<int64_t, std::string>;
|
||||
|
||||
using set = std::set<scalar>;
|
||||
struct dict_value;
|
||||
using dict = std::map<std::string, dict_value>;
|
||||
using dict_variant = std::variant<dict, set, scalar>;
|
||||
struct dict_value : dict_variant {
|
||||
using dict_variant::dict_variant;
|
||||
using dict_variant::operator=;
|
||||
};
|
||||
|
||||
// Helpers for gcc-10 and earlier which don't like visiting a std::variant subtype:
|
||||
constexpr inline dict_variant& unwrap(dict_value& v) {
|
||||
return static_cast<dict_variant&>(v);
|
||||
}
|
||||
constexpr inline const dict_variant& unwrap(const dict_value& v) {
|
||||
return static_cast<const dict_variant&>(v);
|
||||
}
|
||||
|
||||
using hash_t = std::array<unsigned char, 32>;
|
||||
using seqno_hash_t = std::pair<seqno_t, hash_t>;
|
||||
|
||||
class MutableConfigMessage;
|
||||
|
||||
/// Base type for all errors that can happen during config parsing
|
||||
struct config_error : std::runtime_error {
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
/// Type thrown for bad signatures (bad or missing signature).
|
||||
struct signature_error : config_error {
|
||||
using config_error::config_error;
|
||||
};
|
||||
/// Type thrown for a missing signature when a signature is required.
|
||||
struct missing_signature : signature_error {
|
||||
using signature_error::signature_error;
|
||||
};
|
||||
/// Type thrown for an unparseable config (e.g. keys with invalid types, or keys before "#" or after
|
||||
/// "~").
|
||||
struct config_parse_error : config_error {
|
||||
using config_error::config_error;
|
||||
};
|
||||
|
||||
/// Class for a parsed, read-only config message; also serves as the base class of a
|
||||
/// MutableConfigMessage which allows setting values.
|
||||
class ConfigMessage {
|
||||
public:
|
||||
using lagged_diffs_t = std::map<seqno_hash_t, oxenc::bt_dict>;
|
||||
|
||||
#ifndef SESSION_TESTING_EXPOSE_INTERNALS
|
||||
protected:
|
||||
#endif
|
||||
dict data_;
|
||||
|
||||
// diff data for *this* message, parsed during construction. Subclasses may use this for
|
||||
// managing their own diff in the `diff()` method.
|
||||
oxenc::bt_dict diff_;
|
||||
|
||||
// diffs of previous messages that are included in this message.
|
||||
lagged_diffs_t lagged_diffs_;
|
||||
|
||||
// Unknown top-level config keys which we preserve even though we don't understand what they
|
||||
// mean.
|
||||
oxenc::bt_dict unknown_;
|
||||
|
||||
/// Seqno and hash of the message; we calculate this when loading. Subclasses put the hash here
|
||||
/// (so that they can return a reference to it).
|
||||
seqno_hash_t seqno_hash_{0, {0}};
|
||||
|
||||
bool verified_signature_ = false;
|
||||
|
||||
// This will be set during construction from configs based on the merge result:
|
||||
// -1 means we had to merge one or more configs together into a new merged config
|
||||
// >= 0 indicates the index of the config we used if we did not merge (i.e. there was only one
|
||||
// config, or there were multiple but one of them referenced all the others).
|
||||
int unmerged_ = -1;
|
||||
|
||||
public:
|
||||
constexpr static int DEFAULT_DIFF_LAGS = 5;
|
||||
|
||||
/// Verification function: this is passed the data that should have been signed and the 64-byte
|
||||
/// signature. Should return true to accept the signature, false to reject it and skip the
|
||||
/// message. It can also throw to abort message construction (that is: returning false skips
|
||||
/// the message when loading multiple messages, but can still continue with other messages;
|
||||
/// throwing aborts the entire construction).
|
||||
using verify_callable = std::function<bool(ustring_view data, ustring_view signature)>;
|
||||
|
||||
/// Signing function: this is passed the data to be signed and returns the 64-byte signature.
|
||||
using sign_callable = std::function<ustring(ustring_view data)>;
|
||||
|
||||
ConfigMessage();
|
||||
ConfigMessage(const ConfigMessage&) = default;
|
||||
ConfigMessage& operator=(const ConfigMessage&) = default;
|
||||
ConfigMessage(ConfigMessage&&) = default;
|
||||
ConfigMessage& operator=(ConfigMessage&&) = default;
|
||||
|
||||
virtual ~ConfigMessage() = default;
|
||||
|
||||
/// Initializes a config message by parsing a serialized message. Throws on any error. See the
|
||||
/// vector version below for argument descriptions.
|
||||
explicit ConfigMessage(
|
||||
ustring_view serialized,
|
||||
verify_callable verifier = nullptr,
|
||||
sign_callable signer = nullptr,
|
||||
int lag = DEFAULT_DIFF_LAGS,
|
||||
bool signature_optional = false);
|
||||
|
||||
/// Constructs a new ConfigMessage by loading and potentially merging multiple serialized
|
||||
/// ConfigMessages together, according to the config conflict resolution rules. The result
|
||||
/// of this call can either be one of the config messages directly (if one is found that
|
||||
/// includes all the others), or can be a new config message that merges multiple configs
|
||||
/// together. You can check `.merged()` to see which happened.
|
||||
///
|
||||
/// This constructor always requires at least one valid config from the given inputs; if all are
|
||||
/// empty,
|
||||
///
|
||||
/// verifier - a signature verification function. If provided and not nullptr this will be
|
||||
/// called to verify each signature in the provided messages: any that are missing a signature
|
||||
/// or for which the verifier returns false will be dropped from consideration for merging. If
|
||||
/// *all* messages fail verification an exception is raised.
|
||||
///
|
||||
/// signer - a signature generation function. This is not used directly by the ConfigMessage,
|
||||
/// but providing it will allow it to be passed automatically to any MutableConfigMessage
|
||||
/// derived from this ConfigMessage.
|
||||
///
|
||||
/// lag - the lag setting controlling the config merging rules. Any config message with lagged
|
||||
/// diffs that exceeding this lag value will have those early lagged diffs dropping during
|
||||
/// loading.
|
||||
///
|
||||
/// signature_optional - if true then accept a message with no signature even when a verifier is
|
||||
/// set, thus allowing unsigned messages (though messages with an invalid signature are still
|
||||
/// not allowed). This option is ignored when verifier is not set.
|
||||
///
|
||||
/// error_handler - if set then any config message parsing error will be passed to this function
|
||||
/// for handling with the index of `configs` that failed and the error exception: the callback
|
||||
/// typically warns and, if the overall construction should abort, rethrows the error. If this
|
||||
/// function is omitted then the default skips (without failing) individual parse errors and
|
||||
/// only aborts construction if *all* messages fail to parse. A simple handler such as
|
||||
/// `[](size_t, const auto& e) { throw e; }` can be used to make any parse error of any message
|
||||
/// fatal.
|
||||
explicit ConfigMessage(
|
||||
const std::vector<ustring_view>& configs,
|
||||
verify_callable verifier = nullptr,
|
||||
sign_callable signer = nullptr,
|
||||
int lag = DEFAULT_DIFF_LAGS,
|
||||
bool signature_optional = false,
|
||||
std::function<void(size_t, const config_error&)> error_handler = nullptr);
|
||||
|
||||
/// Returns a read-only reference to the contained data. (To get a mutable config object use
|
||||
/// MutableConfigMessage).
|
||||
const dict& data() const { return data_; }
|
||||
|
||||
/// The verify function; if loading a message with a signature and this is set then it will
|
||||
/// be called to verify the signature of the message. Takes a pointer to the signing data,
|
||||
/// the data length, and a pointer to the 64-byte signature.
|
||||
verify_callable verifier;
|
||||
|
||||
/// The signing function; this is not directly used by the non-mutable base class, but will be
|
||||
/// propagated to mutable config messages that are derived e.g. by calling `.increment()`. This
|
||||
/// is called when serializing a config message to add a signature. If it is nullptr then no
|
||||
/// signature is added to the serialized data.
|
||||
sign_callable signer;
|
||||
|
||||
/// How many lagged config diffs that should be carried forward to resolve conflicts,
|
||||
/// including this message. If 0 then config messages won't have any diffs and will not be
|
||||
/// mergeable.
|
||||
int lag = DEFAULT_DIFF_LAGS;
|
||||
|
||||
/// The diff structure for changes in *this* config message. Subclasses that need to override
|
||||
/// should populate into `diff_` and return a reference to it (internal code assumes `diff_` is
|
||||
/// correct immediately after a call to this).
|
||||
virtual const oxenc::bt_dict& diff();
|
||||
|
||||
/// Returns the seqno of this message
|
||||
const seqno_t& seqno() const { return seqno_hash_.first; }
|
||||
|
||||
/// Calculates the hash of the current message. For a ConfigMessage this is calculated when the
|
||||
/// message is first loaded; for a MutableConfigMessage this serializes the current value to
|
||||
/// properly compute the current hash. Subclasses must ensure that seqno_hash_.second is set to
|
||||
/// the correct value when this is called (and typically return a reference to it).
|
||||
virtual const hash_t& hash() { return seqno_hash_.second; }
|
||||
|
||||
/// After loading multiple config files this flag indicates whether or not we had to produce a
|
||||
/// new, merged configuration message (true) or did not need to merge (false). (For config
|
||||
/// messages that were not loaded from serialized data this is always true).
|
||||
bool merged() const { return unmerged_ == -1; }
|
||||
|
||||
/// After loading multiple config files this field contains the index of the single config we
|
||||
/// used if we didn't need to merge (that is: there was only one config or one config that
|
||||
/// superceded all the others). If we had to merge (or this wasn't loaded from serialized
|
||||
/// data), this will return -1.
|
||||
int unmerged_index() const { return unmerged_; }
|
||||
|
||||
/// Returns true if this message contained a valid, verified signature when it was parsed.
|
||||
/// Returns false otherwise (e.g. not loaded from verification at all; loaded without a
|
||||
/// verification function; or had no signature and a signature wasn't required).
|
||||
bool verified_signature() const { return verified_signature_; }
|
||||
|
||||
/// Constructs a new MutableConfigMessage from this config message with an incremented seqno.
|
||||
/// The new config message's diff will reflect changes made after this construction.
|
||||
virtual MutableConfigMessage increment() const;
|
||||
|
||||
/// Serializes this config's data. Note that if the ConfigMessage was constructed from signed,
|
||||
/// serialized input, this will only produce an exact copy of the original serialized input if
|
||||
/// it uses the identical, deterministic signing function used to construct the original.
|
||||
///
|
||||
/// The optional `enable_signing` argument can be specified as false to disable signing (this is
|
||||
/// typically for a local serialization value that isn't being pushed to the server). Note that
|
||||
/// signing is always disabled if there is no signing callback set, regardless of the value of
|
||||
/// this argument.
|
||||
virtual ustring serialize(bool enable_signing = true);
|
||||
|
||||
protected:
|
||||
ustring serialize_impl(const oxenc::bt_dict& diff, bool enable_signing = true);
|
||||
};
|
||||
|
||||
// Constructor tag
|
||||
struct increment_seqno_t {};
|
||||
struct retain_seqno_t {};
|
||||
inline constexpr increment_seqno_t increment_seqno{};
|
||||
inline constexpr retain_seqno_t retain_seqno{};
|
||||
|
||||
class MutableConfigMessage : public ConfigMessage {
|
||||
protected:
|
||||
dict orig_data_{data_};
|
||||
|
||||
friend class ConfigMessage;
|
||||
|
||||
public:
|
||||
MutableConfigMessage(const MutableConfigMessage&) = default;
|
||||
MutableConfigMessage& operator=(const MutableConfigMessage&) = default;
|
||||
MutableConfigMessage(MutableConfigMessage&&) = default;
|
||||
MutableConfigMessage& operator=(MutableConfigMessage&&) = default;
|
||||
|
||||
/// Constructs a new, empty config message. Takes various fields to pre-fill the various
|
||||
/// properties during construction (these are for convenience and equivalent to setting them via
|
||||
/// properties/methods after construction).
|
||||
///
|
||||
/// seqno -- the message's seqno, default 0
|
||||
/// lags -- number of lags to keep (when deriving messages, e.g. via increment())
|
||||
/// signer -- if specified and not nullptr then this message will be signed when serialized
|
||||
/// using the given signing function. If omitted no signing takes place.
|
||||
explicit MutableConfigMessage(
|
||||
seqno_t seqno = 0, int lag = DEFAULT_DIFF_LAGS, sign_callable signer = nullptr) {
|
||||
this->lag = lag;
|
||||
this->seqno(seqno);
|
||||
this->signer = signer;
|
||||
}
|
||||
|
||||
/// Wraps the ConfigMessage constructor with the same arguments but always produces a
|
||||
/// MutableConfigMessage. In particular this means that if the base constructor performed a
|
||||
/// merge (and thus incremented seqno) then the config stays as is, but contained in a Mutable
|
||||
/// message that can be changed. If it did *not* merge (i.e. the highest seqno message it found
|
||||
/// did not conflict with any other messages) then this construction is equivalent to doing a
|
||||
/// base load followed by a .increment() call. In other words: this constructor *always* gives
|
||||
/// you an incremented seqno value from the highest valid input config message.
|
||||
///
|
||||
/// This is almost equivalent to ConfigMessage{args...}.increment(), except that this
|
||||
/// constructor only increments seqno once while the indirect version would increment twice in
|
||||
/// the case of a required merge conflict resolution.
|
||||
explicit MutableConfigMessage(
|
||||
const std::vector<ustring_view>& configs,
|
||||
verify_callable verifier = nullptr,
|
||||
sign_callable signer = nullptr,
|
||||
int lag = DEFAULT_DIFF_LAGS,
|
||||
bool signature_optional = false,
|
||||
std::function<void(size_t, const config_error&)> error_handler = nullptr);
|
||||
|
||||
/// Wrapper around the above that takes a single string view to load a single message, doesn't
|
||||
/// take an error handler and instead always throws on parse errors (the above also throws for
|
||||
/// an erroneous single message, but with a less specific "no valid config messages" error).
|
||||
explicit MutableConfigMessage(
|
||||
ustring_view config,
|
||||
verify_callable verifier = nullptr,
|
||||
sign_callable signer = nullptr,
|
||||
int lag = DEFAULT_DIFF_LAGS,
|
||||
bool signature_optional = false);
|
||||
|
||||
/// Does the same as the base incrementing, but also records any diff info from the current
|
||||
/// MutableConfigMessage. *this* object gets pruned and signed as part of this call. If the
|
||||
/// sign argument is omitted/nullptr then the current object's `sign` callback gets copied into
|
||||
/// the new object. After this call you typically do not want to further modify *this (because
|
||||
/// any modifications will change the hash, making *this no longer a parent of the new object).
|
||||
MutableConfigMessage increment() const override;
|
||||
|
||||
/// Constructor that does the same thing as the `m.increment()` factory method. The second
|
||||
/// value should be the literal `increment_seqno` value (to select this constructor).
|
||||
explicit MutableConfigMessage(const ConfigMessage& m, const increment_seqno_t&);
|
||||
|
||||
/// Constructor that moves a immutable message into a mutable one, retaining the current seqno.
|
||||
/// This is typically used in situations where the ConfigMessage has had some implicit seqno
|
||||
/// increment already (e.g. from merging) and we want it to become mutable without incrementing
|
||||
/// the seqno again. The second value should be the literal `retain_seqno` value (to select
|
||||
/// this constructor).
|
||||
explicit MutableConfigMessage(ConfigMessage&& m, const retain_seqno_t&);
|
||||
|
||||
using ConfigMessage::data;
|
||||
/// Returns a mutable reference to the underlying config data.
|
||||
dict& data() { return data_; }
|
||||
|
||||
using ConfigMessage::seqno;
|
||||
|
||||
/// Sets the seqno of the message to a specific value. You usually want to use `.increment()`
|
||||
/// from an existing config message rather than manually adjusting the seqno.
|
||||
void seqno(seqno_t new_seqno) { seqno_hash_.first = new_seqno; }
|
||||
|
||||
/// Returns the current diff for this data relative to its original data. The data is pruned
|
||||
/// implicitly by this call.
|
||||
const oxenc::bt_dict& diff() override;
|
||||
|
||||
/// Prunes empty dicts/sets from data. This is called automatically when serializing or
|
||||
/// calculating a diff. Returns true if the data was actually changed, false if nothing needed
|
||||
/// pruning.
|
||||
bool prune();
|
||||
|
||||
/// Calculates the hash of the current message. Can optionally be given the already-serialized
|
||||
/// value, if available; if empty/omitted, `serialize()` will be called to compute it.
|
||||
const hash_t& hash() override;
|
||||
|
||||
protected:
|
||||
const hash_t& hash(ustring_view serialized);
|
||||
void increment_impl();
|
||||
};
|
||||
|
||||
} // namespace session::config
|
||||
|
||||
namespace oxenc::detail {
|
||||
|
||||
template <>
|
||||
struct bt_serialize<session::config::dict_value> : bt_serialize<session::config::dict_variant> {};
|
||||
|
||||
} // namespace oxenc::detail
|
|
@ -1,154 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "../config.h"
|
||||
|
||||
// Config object base type: this type holds the internal object and is initialized by the various
|
||||
// config-dependent settings (e.g. config_user_profile_init) then passed to the various functions.
|
||||
typedef struct config_object {
|
||||
// Internal opaque object pointer; calling code should leave this alone.
|
||||
void* internals;
|
||||
// When an error occurs in the C API this string will be set to the specific error message. May
|
||||
// be empty.
|
||||
const char* last_error;
|
||||
|
||||
// Sometimes used as the backing buffer for `last_error`. Should not be touched externally.
|
||||
char _error_buf[256];
|
||||
} config_object;
|
||||
|
||||
// Common functions callable on any config instance:
|
||||
|
||||
/// Frees a config object created with one of the config-dependent ..._init functions (e.g.
|
||||
/// user_profile_init).
|
||||
void config_free(config_object* conf);
|
||||
|
||||
typedef enum config_log_level {
|
||||
LOG_LEVEL_DEBUG = 0,
|
||||
LOG_LEVEL_INFO,
|
||||
LOG_LEVEL_WARNING,
|
||||
LOG_LEVEL_ERROR
|
||||
} config_log_level;
|
||||
|
||||
/// Sets a logging function; takes the log function pointer and a context pointer (which can be NULL
|
||||
/// if not needed). The given function pointer will be invoked with one of the above values, a
|
||||
/// null-terminated c string containing the log message, and the void* context object given when
|
||||
/// setting the logger (this is for caller-specific state data and won't be touched).
|
||||
///
|
||||
/// The logging function must have signature:
|
||||
///
|
||||
/// void log(config_log_level lvl, const char* msg, void* ctx);
|
||||
///
|
||||
/// Can be called with callback set to NULL to clear an existing logger.
|
||||
///
|
||||
/// The config object itself has no log level: the caller should filter by level as needed.
|
||||
void config_set_logger(
|
||||
config_object* conf, void (*callback)(config_log_level, const char*, void*), void* ctx);
|
||||
|
||||
/// Returns the numeric namespace in which config messages of this type should be stored.
|
||||
int16_t config_storage_namespace(const config_object* conf);
|
||||
|
||||
/// Merges the config object with one or more remotely obtained config strings. After this call the
|
||||
/// config object may be unchanged, complete replaced, or updated and needing a push, depending on
|
||||
/// the messages that are merged; the caller should check config_needs_push().
|
||||
///
|
||||
/// `msg_hashes` is an array of null-terminated C strings containing the hashes of the configs being
|
||||
/// provided.
|
||||
/// `configs` is an array of pointers to the start of the (binary) data.
|
||||
/// `lengths` is an array of lengths of the binary data
|
||||
/// `count` is the length of all three arrays.
|
||||
int config_merge(
|
||||
config_object* conf,
|
||||
const char** msg_hashes,
|
||||
const unsigned char** configs,
|
||||
const size_t* lengths,
|
||||
size_t count);
|
||||
|
||||
/// Returns true if this config object contains updated data that has not yet been confirmed stored
|
||||
/// on the server.
|
||||
bool config_needs_push(const config_object* conf);
|
||||
|
||||
/// Returned struct of config push data.
|
||||
typedef struct config_push_data {
|
||||
// The config seqno (to be provided later in `config_confirm_pushed`).
|
||||
seqno_t seqno;
|
||||
// The config message to push (binary data, not null-terminated).
|
||||
unsigned char* config;
|
||||
// The length of `config`
|
||||
size_t config_len;
|
||||
// Array of obsolete message hashes to delete; each element is a null-terminated C string
|
||||
char** obsolete;
|
||||
// length of `obsolete`
|
||||
size_t obsolete_len;
|
||||
} config_push_data;
|
||||
|
||||
/// Obtains the configuration data that needs to be pushed to the server.
|
||||
///
|
||||
/// Generally this call should be guarded by a call to `config_needs_push`, however it can be used
|
||||
/// to re-obtain the current serialized config even if no push is needed (for example, if the client
|
||||
/// wants to re-submit it after a network error).
|
||||
///
|
||||
/// NB: The returned pointer belongs to the caller: that is, the caller *MUST* free() it when
|
||||
/// done with it.
|
||||
config_push_data* config_push(config_object* conf);
|
||||
|
||||
/// Reports that data obtained from `config_push` has been successfully stored on the server with
|
||||
/// message hash `msg_hash`. The seqno value is the one returned by the config_push call that
|
||||
/// yielded the config data.
|
||||
void config_confirm_pushed(config_object* conf, seqno_t seqno, const char* msg_hash);
|
||||
|
||||
/// Returns a binary dump of the current state of the config object. This dump can be used to
|
||||
/// resurrect the object at a later point (e.g. after a restart). Allocates a new buffer and sets
|
||||
/// it in `out` and the length in `outlen`. Note that this is binary data, *not* a null-terminated
|
||||
/// C string.
|
||||
///
|
||||
/// NB: It is the caller's responsibility to `free()` the buffer when done with it.
|
||||
///
|
||||
/// Immediately after this is called `config_needs_dump` will start returning true (until the
|
||||
/// configuration is next modified).
|
||||
void config_dump(config_object* conf, unsigned char** out, size_t* outlen);
|
||||
|
||||
/// Returns true if something has changed since the last call to `dump()` that requires calling
|
||||
/// and saving the `config_dump()` data again.
|
||||
bool config_needs_dump(const config_object* conf);
|
||||
|
||||
/// Struct containing a list of C strings. Typically where this is returned by this API it must be
|
||||
/// freed (via `free()`) when done with it.
|
||||
typedef struct config_string_list {
|
||||
char** value; // array of null-terminated C strings
|
||||
size_t len; // length of `value`
|
||||
} config_string_list;
|
||||
|
||||
/// Obtains the current active hashes. Note that this will be empty if the current hash is unknown
|
||||
/// or not yet determined (for example, because the current state is dirty or because the most
|
||||
/// recent push is still pending and we don't know the hash yet).
|
||||
///
|
||||
/// The returned pointer belongs to the caller and must be freed via `free()` when done with it.
|
||||
config_string_list* config_current_hashes(const config_object* conf);
|
||||
|
||||
/// Config key management; see the corresponding method docs in base.hpp. All `key` arguments here
|
||||
/// are 32-byte binary buffers (and since fixed-length, there is no keylen argument).
|
||||
void config_add_key(config_object* conf, const unsigned char* key);
|
||||
void config_add_key_low_prio(config_object* conf, const unsigned char* key);
|
||||
int config_clear_keys(config_object* conf);
|
||||
bool config_remove_key(config_object* conf, const unsigned char* key);
|
||||
int config_key_count(const config_object* conf);
|
||||
bool config_has_key(const config_object* conf, const unsigned char* key);
|
||||
// Returns a pointer to the 32-byte binary key at position i. This is *not* null terminated (and is
|
||||
// exactly 32 bytes long). `i < config_key_count(conf)` must be satisfied. Ownership of the data
|
||||
// remains in the object (that is: the caller must not attempt to free it).
|
||||
const unsigned char* config_key(const config_object* conf, size_t i);
|
||||
|
||||
/// Returns the encryption domain C-str used to encrypt values for this config object. (This is
|
||||
/// here only for debugging/testing).
|
||||
const char* config_encryption_domain(const config_object* conf);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
|
@ -1,650 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <cassert>
|
||||
#include <memory>
|
||||
#include <session/config.hpp>
|
||||
#include <type_traits>
|
||||
#include <unordered_set>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include "base.h"
|
||||
#include "namespaces.hpp"
|
||||
|
||||
namespace session::config {
|
||||
|
||||
template <typename T, typename... U>
|
||||
static constexpr bool is_one_of = (std::is_same_v<T, U> || ...);
|
||||
|
||||
/// True for a dict_value direct subtype, but not scalar sub-subtypes.
|
||||
template <typename T>
|
||||
static constexpr bool is_dict_subtype = is_one_of<T, config::scalar, config::set, config::dict>;
|
||||
|
||||
/// True for a dict_value or any of the types containable within a dict value
|
||||
template <typename T>
|
||||
static constexpr bool is_dict_value =
|
||||
is_dict_subtype<T> || is_one_of<T, dict_value, int64_t, std::string>;
|
||||
|
||||
// Levels for the logging callback
|
||||
enum class LogLevel { debug = 0, info, warning, error };
|
||||
|
||||
/// Our current config state
|
||||
enum class ConfigState : int {
|
||||
/// Clean means the config is confirmed stored on the server and we haven't changed anything.
|
||||
Clean = 0,
|
||||
|
||||
/// Dirty means we have local changes, and the changes haven't been serialized yet for sending
|
||||
/// to the server.
|
||||
Dirty = 1,
|
||||
|
||||
/// Waiting is halfway in-between clean and dirty: the caller has serialized the data, but
|
||||
/// hasn't yet reported back that the data has been stored, *and* we haven't made any changes
|
||||
/// since the data was serialize.
|
||||
Waiting = 2,
|
||||
};
|
||||
|
||||
/// Base config type for client-side configs containing common functionality needed by all config
|
||||
/// sub-types.
|
||||
class ConfigBase {
|
||||
private:
|
||||
// The object (either base config message or MutableConfigMessage) that stores the current
|
||||
// config message. Subclasses do not directly access this: instead they call `dirty()` if they
|
||||
// intend to make changes, or the `set_config_field` wrapper.
|
||||
std::unique_ptr<ConfigMessage> _config;
|
||||
|
||||
// Tracks our current state
|
||||
ConfigState _state = ConfigState::Clean;
|
||||
|
||||
static constexpr size_t KEY_SIZE = 32;
|
||||
|
||||
// Contains the base key(s) we use to encrypt/decrypt messages. If non-empty, the .front()
|
||||
// element will be used when encrypting a new message to push. When decrypting, we attempt each
|
||||
// of them, starting with .front(), until decryption succeeds.
|
||||
using Key = std::array<unsigned char, KEY_SIZE>;
|
||||
Key* _keys = nullptr;
|
||||
size_t _keys_size = 0;
|
||||
size_t _keys_capacity = 0;
|
||||
|
||||
// Contains the current active message hash, as fed into us in `confirm_pushed()`. Empty if we
|
||||
// don't know it yet. When we dirty the config this value gets moved into `old_hashes_` to be
|
||||
// removed by the next push.
|
||||
std::string _curr_hash;
|
||||
|
||||
// Contains obsolete known message hashes that are obsoleted by the most recent merge or push;
|
||||
// these are returned (and cleared) when `push` is called.
|
||||
std::unordered_set<std::string> _old_hashes;
|
||||
|
||||
protected:
|
||||
// Constructs a base config by loading the data from a dump as produced by `dump()`. If the
|
||||
// dump is nullopt then an empty base config is constructed with no config settings and seqno
|
||||
// set to 0.
|
||||
explicit ConfigBase(std::optional<ustring_view> dump = std::nullopt);
|
||||
|
||||
// Tracks whether we need to dump again; most mutating methods should set this to true (unless
|
||||
// calling set_state, which sets to to true implicitly).
|
||||
bool _needs_dump = false;
|
||||
|
||||
// Sets the current state; this also sets _needs_dump to true. If transitioning to a dirty
|
||||
// state and we know our current message hash, that hash gets added to `old_hashes_` to be
|
||||
// deleted at the next push.
|
||||
void set_state(ConfigState s);
|
||||
|
||||
// Invokes the `logger` callback if set, does nothing if there is no logger.
|
||||
void log(LogLevel lvl, std::string msg) {
|
||||
if (logger)
|
||||
logger(lvl, std::move(msg));
|
||||
}
|
||||
|
||||
// Returns a reference to the current MutableConfigMessage. If the current message is not
|
||||
// already dirty (i.e. Clean or Waiting) then calling this increments the seqno counter.
|
||||
MutableConfigMessage& dirty();
|
||||
|
||||
public:
|
||||
// class for proxying subfield access; this class should never be stored but only used
|
||||
// ephemerally (most of its methods are rvalue-qualified). This lets constructs such as
|
||||
// foo["abc"]["def"]["ghi"] = 12;
|
||||
// work, auto-vivifying (or trampling, if not a dict) subdicts to reach the target. It also
|
||||
// allows non-vivifying value retrieval via .string(), .integer(), etc. methods.
|
||||
class DictFieldProxy {
|
||||
private:
|
||||
ConfigBase& _conf;
|
||||
std::vector<std::string> _inter_keys;
|
||||
std::string _last_key;
|
||||
|
||||
// See if we can find the key without needing to create anything, so that we can attempt to
|
||||
// access values without mutating anything (which allows, among other things, for assigning
|
||||
// of the existing value to not dirty anything). Returns nullptrs if the value or something
|
||||
// along its path would need to be created, or has the wrong type; otherwise a const pointer
|
||||
// to the key and the value. The templated type, if provided, can be one of the types a
|
||||
// dict_value can hold to also check that the returned value has a particular type; if
|
||||
// omitted you get back the dict_value pointer itself. If the field exists but is not the
|
||||
// requested `T` type, you get back the key string pointer with a nullptr value.
|
||||
template <typename T = dict_value, typename = std::enable_if_t<is_dict_value<T>>>
|
||||
std::pair<const std::string*, const T*> get_clean_pair() const {
|
||||
const config::dict* data = &_conf._config->data();
|
||||
// All but the last need to be dicts:
|
||||
for (const auto& key : _inter_keys) {
|
||||
auto it = data->find(key);
|
||||
data = it != data->end() ? std::get_if<config::dict>(&it->second) : nullptr;
|
||||
if (!data)
|
||||
return {nullptr, nullptr};
|
||||
}
|
||||
|
||||
const std::string* key;
|
||||
const dict_value* val;
|
||||
// The last can be any value type:
|
||||
if (auto it = data->find(_last_key); it != data->end()) {
|
||||
key = &it->first;
|
||||
val = &it->second;
|
||||
} else
|
||||
return {nullptr, nullptr};
|
||||
|
||||
if constexpr (std::is_same_v<T, dict_value>)
|
||||
return {key, val};
|
||||
else if constexpr (is_dict_subtype<T>) {
|
||||
return {key, std::get_if<T>(val)};
|
||||
} else { // int64 or std::string, i.e. the config::scalar sub-types.
|
||||
if (auto* scalar = std::get_if<config::scalar>(val))
|
||||
return {key, std::get_if<T>(scalar)};
|
||||
return {key, nullptr};
|
||||
}
|
||||
}
|
||||
|
||||
// Same as above but just gives back the value, not the key
|
||||
template <typename T = dict_value, typename = std::enable_if_t<is_dict_value<T>>>
|
||||
const T* get_clean() const {
|
||||
return get_clean_pair<T>().second;
|
||||
}
|
||||
|
||||
// Returns a lvalue reference to the value, stomping its way through the dict as it goes to
|
||||
// create subdicts as needed to reach the target value. If given a template type then we
|
||||
// also cast the final dict_value variant into the given type (and replace if with a
|
||||
// default-constructed value if it has the wrong type) then return a reference to that.
|
||||
template <typename T = dict_value, typename = std::enable_if_t<is_dict_value<T>>>
|
||||
T& get_dirty() {
|
||||
config::dict* data = &_conf.dirty().data();
|
||||
for (const auto& key : _inter_keys) {
|
||||
auto& val = (*data)[key];
|
||||
data = std::get_if<config::dict>(&val);
|
||||
if (!data)
|
||||
data = &val.emplace<config::dict>();
|
||||
}
|
||||
auto& val = (*data)[_last_key];
|
||||
|
||||
if constexpr (std::is_same_v<T, dict_value>)
|
||||
return val;
|
||||
else if constexpr (is_dict_subtype<T>) {
|
||||
if (auto* v = std::get_if<T>(&val))
|
||||
return *v;
|
||||
return val.emplace<T>();
|
||||
} else { // int64 or std::string, i.e. the config::scalar sub-types.
|
||||
if (auto* scalar = std::get_if<config::scalar>(&val)) {
|
||||
if (auto* v = std::get_if<T>(scalar))
|
||||
return *v;
|
||||
return scalar->emplace<T>();
|
||||
}
|
||||
return val.emplace<scalar>().emplace<T>();
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void assign_if_changed(T value) {
|
||||
// Try to avoiding dirtying the config if this assignment isn't changing anything
|
||||
if (!_conf.is_dirty())
|
||||
if (auto current = get_clean<T>(); current && *current == value)
|
||||
return;
|
||||
|
||||
get_dirty<T>() = std::move(value);
|
||||
}
|
||||
|
||||
void insert_if_missing(config::scalar&& value) {
|
||||
if (!_conf.is_dirty())
|
||||
if (auto current = get_clean<config::set>(); current && current->count(value))
|
||||
return;
|
||||
|
||||
get_dirty<config::set>().insert(std::move(value));
|
||||
}
|
||||
|
||||
void set_erase_impl(const config::scalar& value) {
|
||||
if (!_conf.is_dirty())
|
||||
if (auto current = get_clean<config::set>(); current && !current->count(value))
|
||||
return;
|
||||
|
||||
config::dict* data = &_conf.dirty().data();
|
||||
|
||||
for (const auto& key : _inter_keys) {
|
||||
auto it = data->find(key);
|
||||
data = it != data->end() ? std::get_if<config::dict>(&it->second) : nullptr;
|
||||
if (!data)
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = data->find(_last_key);
|
||||
if (it == data->end())
|
||||
return;
|
||||
auto& val = it->second;
|
||||
if (auto* current = std::get_if<config::set>(&val))
|
||||
current->erase(value);
|
||||
else
|
||||
val.emplace<config::set>();
|
||||
}
|
||||
|
||||
public:
|
||||
DictFieldProxy(ConfigBase& b, std::string key) : _conf{b}, _last_key{std::move(key)} {}
|
||||
|
||||
/// Descends into a dict, returning a copied proxy object for the path to the requested
|
||||
/// field. Nothing is created by doing this unless you actually assign to a value.
|
||||
DictFieldProxy operator[](std::string subkey) const& {
|
||||
DictFieldProxy subfield{_conf, std::move(subkey)};
|
||||
subfield._inter_keys.reserve(_inter_keys.size() + 1);
|
||||
subfield._inter_keys.insert(
|
||||
subfield._inter_keys.end(), _inter_keys.begin(), _inter_keys.end());
|
||||
subfield._inter_keys.push_back(_last_key);
|
||||
return subfield;
|
||||
}
|
||||
|
||||
// Same as above, but when called on an rvalue reference we just mutate the current proxy to
|
||||
// the new dict path.
|
||||
DictFieldProxy&& operator[](std::string subkey) && {
|
||||
_inter_keys.push_back(std::move(_last_key));
|
||||
_last_key = std::move(subkey);
|
||||
return std::move(*this);
|
||||
}
|
||||
|
||||
/// Returns a pointer to the (deepest level) key for this dict pair *if* a pair exists at
|
||||
/// the given location, nullptr otherwise. This allows a caller to get a reference to the
|
||||
/// actual key, rather than an ephemeral copy of the current key value.
|
||||
const std::string* key() const { return get_clean_pair().first; }
|
||||
|
||||
/// Returns a const pointer to the string if one exists at the given location, nullptr
|
||||
/// otherwise.
|
||||
const std::string* string() const { return get_clean<std::string>(); }
|
||||
|
||||
/// Returns the value as a ustring_view, if it exists and is a string; nullopt otherwise.
|
||||
std::optional<ustring_view> uview() const {
|
||||
if (auto* s = get_clean<std::string>())
|
||||
return ustring_view{reinterpret_cast<const unsigned char*>(s->data()), s->size()};
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/// returns the value as a string_view or a fallback if the value doesn't exist (or isn't a
|
||||
/// string). The returned view is directly into the value (or fallback) and so mustn't be
|
||||
/// used beyond the validity of either.
|
||||
std::string_view string_view_or(std::string_view fallback) const {
|
||||
if (auto* s = string())
|
||||
return {*s};
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/// Returns a copy of the value as a string, if it exists and is a string; returns
|
||||
/// `fallback` otherwise.
|
||||
std::string string_or(std::string fallback) const {
|
||||
if (auto* s = string())
|
||||
return *s;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/// Returns a const pointer to the integer if one exists at the given location, nullptr
|
||||
/// otherwise.
|
||||
const int64_t* integer() const { return get_clean<int64_t>(); }
|
||||
|
||||
/// Returns the value as an integer or a fallback if the value doesn't exist (or isn't an
|
||||
/// integer).
|
||||
int64_t integer_or(int64_t fallback) const {
|
||||
if (auto* i = integer())
|
||||
return *i;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/// Returns a const pointer to the set if one exists at the given location, nullptr
|
||||
/// otherwise.
|
||||
const config::set* set() const { return get_clean<config::set>(); }
|
||||
/// Returns a const pointer to the dict if one exists at the given location, nullptr
|
||||
/// otherwise. (You typically don't need to use this but can rather just use [] to descend
|
||||
/// into the dict).
|
||||
const config::dict* dict() const { return get_clean<config::dict>(); }
|
||||
|
||||
/// Replaces the current value with the given string. This also auto-vivifies any
|
||||
/// intermediate dicts needed to reach the given key, including replacing non-dict values if
|
||||
/// they currently exist along the path.
|
||||
void operator=(std::string&& value) { assign_if_changed(std::move(value)); }
|
||||
/// Same as above, but takes a string_view for convenience (this makes a copy).
|
||||
void operator=(std::string_view value) { *this = std::string{value}; }
|
||||
/// Same as above, but takes a ustring_view
|
||||
void operator=(ustring_view value) {
|
||||
*this = std::string{reinterpret_cast<const char*>(value.data()), value.size()};
|
||||
}
|
||||
/// Replace the current value with the given integer. See above.
|
||||
void operator=(int64_t value) { assign_if_changed(value); }
|
||||
/// Replace the current value with the given set. See above.
|
||||
void operator=(config::set value) { assign_if_changed(std::move(value)); }
|
||||
/// Replace the current value with the given dict. See above. This often isn't needed
|
||||
/// because of how other assignment operations work.
|
||||
void operator=(config::dict value) { assign_if_changed(std::move(value)); }
|
||||
|
||||
/// Returns true if there is a value at the current key. If a template type T is given, it
|
||||
/// only returns true if that value also is a `T`.
|
||||
template <typename T = dict_value, typename = std::enable_if_t<is_dict_value<T>>>
|
||||
bool exists() const {
|
||||
return get_clean<T>() != nullptr;
|
||||
}
|
||||
|
||||
// Alias for `exists<T>()`
|
||||
template <typename T>
|
||||
bool is() const {
|
||||
return exists<T>();
|
||||
}
|
||||
|
||||
/// Removes the value at the current location, regardless of what it currently is. This
|
||||
/// does nothing if the current location does not have a value.
|
||||
void erase() {
|
||||
if (!_conf.is_dirty() && !get_clean())
|
||||
return;
|
||||
|
||||
config::dict* data = &_conf.dirty().data();
|
||||
for (const auto& key : _inter_keys) {
|
||||
auto it = data->find(key);
|
||||
data = it != data->end() ? std::get_if<config::dict>(&it->second) : nullptr;
|
||||
if (!data)
|
||||
return;
|
||||
}
|
||||
data->erase(_last_key);
|
||||
}
|
||||
|
||||
/// Adds a value to the set at the current location. If the current value is not a set or
|
||||
/// does not exist then dicts will be created to reach it and a new set will be created.
|
||||
void set_insert(std::string_view value) {
|
||||
insert_if_missing(config::scalar{std::string{value}});
|
||||
}
|
||||
void set_insert(int64_t value) { insert_if_missing(config::scalar{value}); }
|
||||
|
||||
/// Removes a value from the set at the current location. If the current value does not
|
||||
/// exist then nothing happens. If it does exist, but is not a set, it will be replaced
|
||||
/// with an empty set. Otherwise the given value will be removed from the set, if present.
|
||||
void set_erase(std::string_view value) {
|
||||
set_erase_impl(config::scalar{std::string{value}});
|
||||
}
|
||||
void set_erase(int64_t value) { set_erase_impl(scalar{value}); }
|
||||
|
||||
/// Emplaces a value at the current location. As with assignment, this creates dicts as
|
||||
/// needed along the keys to reach the target. The existing value (if present) is destroyed
|
||||
/// to make room for the new one.
|
||||
template <
|
||||
typename T,
|
||||
typename... Args,
|
||||
typename = std::enable_if_t<
|
||||
is_one_of<T, config::set, config::dict, int64_t, std::string>>>
|
||||
T& emplace(Args&&... args) {
|
||||
if constexpr (is_one_of<T, int64_t, std::string>)
|
||||
return get_dirty<scalar>().emplace<T>(std::forward<Args>(args)...);
|
||||
|
||||
return get_dirty().emplace<T>(std::forward<Args>(args)...);
|
||||
}
|
||||
};
|
||||
|
||||
/// Wrapper for the ConfigBase's root `data` field to provide data access. Only provides a []
|
||||
/// that gets you into a DictFieldProxy.
|
||||
class DictFieldRoot {
|
||||
ConfigBase& _conf;
|
||||
DictFieldRoot(DictFieldRoot&&) = delete;
|
||||
DictFieldRoot(const DictFieldRoot&) = delete;
|
||||
DictFieldRoot& operator=(DictFieldRoot&&) = delete;
|
||||
DictFieldRoot& operator=(const DictFieldRoot&) = delete;
|
||||
|
||||
public:
|
||||
DictFieldRoot(ConfigBase& b) : _conf{b} {}
|
||||
|
||||
/// Access a dict element. This returns a proxy object for accessing the value, but does
|
||||
/// *not* auto-vivify the path (unless/until you assign to it).
|
||||
DictFieldProxy operator[](std::string key) const& {
|
||||
return DictFieldProxy{_conf, std::move(key)};
|
||||
}
|
||||
};
|
||||
|
||||
protected:
|
||||
// Called when dumping to obtain any extra data that a subclass needs to store to reconstitute
|
||||
// the object. The base implementation does nothing. The counterpart to this,
|
||||
// `load_extra_data()`, is called when loading from a dump that has extra data; a subclass
|
||||
// should either override both (if it needs to serialize extra data) or neither (if it needs no
|
||||
// extra data). Internally this extra data (if non-empty) is stored in the "+" key of the dump.
|
||||
virtual oxenc::bt_dict extra_data() const { return {}; }
|
||||
|
||||
// Called when constructing from a dump that has extra data. The base implementation does
|
||||
// nothing.
|
||||
virtual void load_extra_data(oxenc::bt_dict extra) {}
|
||||
|
||||
// Called to load an ed25519 key for encryption; this is meant for use by single-ownership
|
||||
// config types, like UserProfile, but not shared config types (closed groups).
|
||||
//
|
||||
// Takes a binary string which is either the 32-byte seed, or 64-byte libsodium secret (which is
|
||||
// just the seed and pubkey concatenated together), and then calls `key(...)` with the seed.
|
||||
// Throws std::invalid_argument if given something that doesn't match the required input.
|
||||
void load_key(ustring_view ed25519_secretkey);
|
||||
|
||||
public:
|
||||
virtual ~ConfigBase();
|
||||
|
||||
// Proxy class providing read and write access to the contained config data.
|
||||
const DictFieldRoot data{*this};
|
||||
|
||||
// If set then we log things by calling this callback
|
||||
std::function<void(LogLevel lvl, std::string msg)> logger;
|
||||
|
||||
// Accesses the storage namespace where this config type is to be stored/loaded from. See
|
||||
// namespaces.hpp for the underlying integer values.
|
||||
virtual Namespace storage_namespace() const = 0;
|
||||
|
||||
/// Subclasses must override this to return a constant string that is unique per config type;
|
||||
/// this value is used for domain separation in encryption. The string length must be between 1
|
||||
/// and 24 characters; use the class name (e.g. "UserProfile") unless you have something better
|
||||
/// to use. This is rarely needed externally; it is public merely for testing purposes.
|
||||
virtual const char* encryption_domain() const = 0;
|
||||
|
||||
/// The zstd compression level to use for this type. Subclasses can override this if they have
|
||||
/// some particular special compression level, or to disable compression entirely (by returning
|
||||
/// std::nullopt). The default is zstd level 1.
|
||||
virtual std::optional<int> compression_level() const { return 1; }
|
||||
|
||||
// How many config lags should be used for this object; default to 5. Implementing subclasses
|
||||
// can override to return a different constant if desired. More lags require more "diff"
|
||||
// storage in the config messages, but also allow for a higher tolerance of simultaneous message
|
||||
// conflicts.
|
||||
virtual int config_lags() const { return 5; }
|
||||
|
||||
// This takes all of the messages pulled down from the server and does whatever is necessary to
|
||||
// merge (or replace) the current values.
|
||||
//
|
||||
// Values are pairs of the message hash (as provided by the server) and the raw message body.
|
||||
//
|
||||
// After this call the caller should check `needs_push()` to see if the data on hand was updated
|
||||
// and needs to be pushed to the server again (for example, because the data contained conflicts
|
||||
// that required another update to resolve).
|
||||
//
|
||||
// Returns the number of the given config messages that were successfully parsed.
|
||||
//
|
||||
// Will throw on serious error (i.e. if neither the current nor any of the given configs are
|
||||
// parseable). This should not happen (the current config, at least, should always be
|
||||
// re-parseable).
|
||||
virtual int merge(const std::vector<std::pair<std::string, ustring_view>>& configs);
|
||||
|
||||
// Same as above but takes the values as ustring's as sometimes that is more convenient.
|
||||
int merge(const std::vector<std::pair<std::string, ustring>>& configs);
|
||||
|
||||
// Returns true if we are currently dirty (i.e. have made changes that haven't been serialized
|
||||
// yet).
|
||||
bool is_dirty() const { return _state == ConfigState::Dirty; }
|
||||
|
||||
// Returns true if we are curently clean (i.e. our current config is stored on the server and
|
||||
// unmodified).
|
||||
bool is_clean() const { return _state == ConfigState::Clean; }
|
||||
|
||||
// The current config hash(es); this can be empty if the current hash is unknown or the current
|
||||
// state is not clean (i.e. a push is needed or pending).
|
||||
std::vector<std::string> current_hashes() const;
|
||||
|
||||
// Returns true if this object contains updated data that has not yet been confirmed stored on
|
||||
// the server. This will be true whenever `is_clean()` is false: that is, if we are currently
|
||||
// "dirty" (i.e. have changes that haven't been pushed) or are still awaiting confirmation of
|
||||
// storage of the most recent serialized push data.
|
||||
virtual bool needs_push() const;
|
||||
|
||||
// Returns a tuple of three elements:
|
||||
// - the seqno value of the data
|
||||
// - the data message to push to the server
|
||||
// - a list of known message hashes that are obsoleted by this push.
|
||||
//
|
||||
// Additionally, if the internal state is currently dirty (i.e. there are unpushed changes), the
|
||||
// internal state will be marked as awaiting-confirmation. Any further data changes made after
|
||||
// this call will re-dirty the data (incrementing seqno and requiring another push).
|
||||
//
|
||||
// The client is expected to send a sequence request to the server that stores the message and
|
||||
// deletes the hashes (if any). It is strongly recommended to use a sequence rather than a
|
||||
// batch so that the deletions won't happen if the store fails for some reason.
|
||||
//
|
||||
// Upon successful completion of the store+deletion requests the client should call
|
||||
// `confirm_pushed` with the seqno value to confirm that the message has been stored.
|
||||
//
|
||||
// Subclasses that need to perform pre-push tasks (such as pruning stale data) can override this
|
||||
// to prune and then call the base method to perform the actual push generation.
|
||||
virtual std::tuple<seqno_t, ustring, std::vector<std::string>> push();
|
||||
|
||||
// Should be called after the push is confirmed stored on the storage server swarm to let the
|
||||
// object know the config message has been stored and, ideally, that the obsolete messages
|
||||
// returned by `push()` are deleted. Once this is called `needs_push` will start returning
|
||||
// false until something changes. Takes the seqno that was pushed so that the object can ensure
|
||||
// that the latest version was pushed (i.e. in case there have been other changes since the
|
||||
// `push()` call that returned this seqno).
|
||||
//
|
||||
// Ideally the caller should have both stored the returned message and deleted the given
|
||||
// messages. The deletion step isn't critical (it is just cleanup) and callers should call this
|
||||
// as long as the store succeeded even if there were errors in the deletions.
|
||||
//
|
||||
// It is safe to call this multiple times with the same seqno value, and with out-of-order
|
||||
// seqnos (e.g. calling with seqno 122 after having called with 123; the duplicates and earlier
|
||||
// ones will just be ignored).
|
||||
virtual void confirm_pushed(seqno_t seqno, std::string msg_hash);
|
||||
|
||||
// Returns a dump of the current state for storage in the database; this value would get passed
|
||||
// into the constructor to reconstitute the object (including the push/not pushed status). This
|
||||
// method is *not* virtual: if subclasses need to store extra data they should set it in the
|
||||
// `subclass_data` field.
|
||||
ustring dump();
|
||||
|
||||
// Returns true if something has changed since the last call to `dump()` that requires calling
|
||||
// and saving the `dump()` data again.
|
||||
virtual bool needs_dump() const { return _needs_dump; }
|
||||
|
||||
// Encryption key methods. For classes that have a single, static key (such as user profile
|
||||
// storage types) these methods typically don't need to be used: the subclass calls them
|
||||
// automatically.
|
||||
|
||||
// Adds an encryption/decryption key, without removing existing keys. They key must be exactly
|
||||
// 32 bytes long. The newly added key becomes the highest priority key (unless the
|
||||
// `high_priority` argument is set to false' see below): it will be used for encryption of
|
||||
// config pushes after the call, and will be tried first when decrypting, followed by keys
|
||||
// present (if any) before this call. If the given key is already present in the key list then
|
||||
// this call moves it to the front of the list (if not already at the front).
|
||||
//
|
||||
// If the `high_priority` argument is specified and false, then the key is added to the *end* of
|
||||
// the key list instead of the beginning: that is, it will not replace the current
|
||||
// highest-priority key used for encryption, but will still be usable for decryption of new
|
||||
// incoming messages (after trying keys present before the call). If the key already exists
|
||||
// then nothing happens with `high_priority=false` (in particular, it is *not* repositioned, in
|
||||
// contrast to high_priority=true behaviour).
|
||||
//
|
||||
// Will throw a std::invalid_argument if the key is not 32 bytes.
|
||||
void add_key(ustring_view key, bool high_priority = true);
|
||||
|
||||
// Clears all stored encryption/decryption keys. This is typically immediately followed with
|
||||
// one or more `add_key` call to replace existing keys. Returns the number of keys removed.
|
||||
int clear_keys();
|
||||
|
||||
// Removes the given encryption/decryption key, if present. Returns true if it was found and
|
||||
// removed, false if it was not in the key list.
|
||||
//
|
||||
// The optional second argument removes the key only from position `from` or higher. It is
|
||||
// mainly for internal use and is usually omitted.
|
||||
bool remove_key(ustring_view key, size_t from = 0);
|
||||
|
||||
// Returns a vector of encryption keys, in priority order (i.e. element 0 is the encryption key,
|
||||
// and the first decryption key).
|
||||
std::vector<ustring_view> get_keys() const;
|
||||
|
||||
// Returns the number of encryption keys.
|
||||
int key_count() const;
|
||||
|
||||
// Returns true if the given key is already in the keys list.
|
||||
bool has_key(ustring_view key) const;
|
||||
|
||||
// Accesses the key at position i (0 if omitted). There must be at least one key, and i must be
|
||||
// less than key_count(). The key at position 0 is used for encryption; for decryption all keys
|
||||
// are tried in order, starting from position 0.
|
||||
ustring_view key(size_t i = 0) const {
|
||||
assert(i < _keys_size);
|
||||
return {_keys[i].data(), _keys[i].size()};
|
||||
}
|
||||
};
|
||||
|
||||
// The C++ struct we hold opaquely inside the C internals struct. This is designed so that any
|
||||
// internals<T> has the same layout so that it doesn't matter whether we unbox to an
|
||||
// internals<ConfigBase> or internals<SubType>.
|
||||
template <
|
||||
typename ConfigT = ConfigBase,
|
||||
std::enable_if_t<std::is_base_of_v<ConfigBase, ConfigT>, int> = 0>
|
||||
struct internals final {
|
||||
std::unique_ptr<ConfigBase> config;
|
||||
std::string error;
|
||||
|
||||
// Dereferencing falls through to the ConfigBase object
|
||||
ConfigT* operator->() {
|
||||
if constexpr (std::is_same_v<ConfigT, ConfigBase>)
|
||||
return config.get();
|
||||
else {
|
||||
auto* c = dynamic_cast<ConfigT*>(config.get());
|
||||
assert(c);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
const ConfigT* operator->() const {
|
||||
if constexpr (std::is_same_v<ConfigT, ConfigBase>)
|
||||
return config.get();
|
||||
else {
|
||||
auto* c = dynamic_cast<ConfigT*>(config.get());
|
||||
assert(c);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
ConfigT& operator*() { return *operator->(); }
|
||||
const ConfigT& operator*() const { return *operator->(); }
|
||||
};
|
||||
|
||||
template <typename T = ConfigBase, std::enable_if_t<std::is_base_of_v<ConfigBase, T>, int> = 0>
|
||||
inline internals<T>& unbox(config_object* conf) {
|
||||
return *static_cast<internals<T>*>(conf->internals);
|
||||
}
|
||||
template <typename T = ConfigBase, std::enable_if_t<std::is_base_of_v<ConfigBase, T>, int> = 0>
|
||||
inline const internals<T>& unbox(const config_object* conf) {
|
||||
return *static_cast<const internals<T>*>(conf->internals);
|
||||
}
|
||||
|
||||
// Sets an error message in the internals.error string and updates the last_error pointer in the
|
||||
// outer (C) config_object struct to point at it.
|
||||
void set_error(config_object* conf, std::string e);
|
||||
|
||||
// Same as above, but gets the error string out of an exception and passed through a return value.
|
||||
// Intended to simplify catch-and-return-error such as:
|
||||
// try {
|
||||
// whatever();
|
||||
// } catch (const std::exception& e) {
|
||||
// return set_error(conf, LIB_SESSION_ERR_OHNOES, e);
|
||||
// }
|
||||
inline int set_error(config_object* conf, int errcode, const std::exception& e) {
|
||||
set_error(conf, e.what());
|
||||
return errcode;
|
||||
}
|
||||
|
||||
// Copies a value contained in a string into a new malloced char buffer, returning the buffer and
|
||||
// size via the two pointer arguments.
|
||||
void copy_out(ustring_view data, unsigned char** out, size_t* outlen);
|
||||
|
||||
} // namespace session::config
|
|
@ -1,48 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
// Maximum string length of a community base URL
|
||||
extern const size_t COMMUNITY_BASE_URL_MAX_LENGTH;
|
||||
|
||||
// Maximum string length of a community room token
|
||||
extern const size_t COMMUNITY_ROOM_MAX_LENGTH;
|
||||
|
||||
// Maximum string length of a full URL as produced by the community_make_full_url() function.
|
||||
// Unlike the above constants, this *includes* space for a NULL string terminator.
|
||||
extern const size_t COMMUNITY_FULL_URL_MAX_LENGTH;
|
||||
|
||||
// Parses a community URL. Writes the canonical base url, room token, and pubkey bytes into the
|
||||
// given pointers. base_url must be at least BASE_URL_MAX_LENGTH+1; room must be at least
|
||||
// ROOM_MAX_LENGTH+1; and pubkey must be (at least) 32 bytes.
|
||||
//
|
||||
// Returns true if the url was parsed successfully, false if parsing failed (i.e. an invalid URL).
|
||||
bool community_parse_full_url(
|
||||
const char* full_url, char* base_url, char* room_token, unsigned char* pubkey);
|
||||
|
||||
// Similar to the above, but allows a URL to omit the pubkey. If no pubkey is found, `pubkey` is
|
||||
// left unchanged and `has_pubkey` is set to false; otherwise `pubkey` is written and `has_pubkey`
|
||||
// is set to true. `pubkey` may be set to NULL, in which case it is never written. `has_pubkey`
|
||||
// may be NULL in which case it is not set (typically both pubkey arguments would be null for cases
|
||||
// where you don't care at all about the pubkey).
|
||||
bool community_parse_partial_url(
|
||||
const char* full_url,
|
||||
char* base_url,
|
||||
char* room_token,
|
||||
unsigned char* pubkey,
|
||||
bool* has_pubkey);
|
||||
|
||||
// Produces a standard full URL from a given base_url (c string), room token (c string), and pubkey
|
||||
// (fixed-length 32 byte buffer). The full URL is written to `full_url`, which must be at least
|
||||
// COMMUNITY_FULL_URL_MAX_LENGTH in size.
|
||||
void community_make_full_url(
|
||||
const char* base_url, const char* room, const unsigned char* pubkey, char* full_url);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
|
@ -1,254 +0,0 @@
|
|||
#pragma once
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <session/config.hpp>
|
||||
#include <session/types.hpp>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
|
||||
namespace session::config {
|
||||
|
||||
/// Base class for types representing a community; this base type handles the url/room/pubkey that
|
||||
/// such a type need. Generally a class inherits from this to extend with the local
|
||||
/// community-related values.
|
||||
struct community {
|
||||
|
||||
// 267 = len('https://') + 253 (max valid DNS name length) + len(':XXXXX')
|
||||
static constexpr size_t BASE_URL_MAX_LENGTH = 267;
|
||||
static constexpr size_t ROOM_MAX_LENGTH = 64;
|
||||
|
||||
community() = default;
|
||||
|
||||
// Constructs an empty community struct from url, room, and pubkey. `base_url` will be
|
||||
// normalized if not already. pubkey is 32 bytes.
|
||||
community(std::string_view base_url, std::string_view room, ustring_view pubkey);
|
||||
|
||||
// Same as above, but takes pubkey as an encoded (hex or base32z or base64) string.
|
||||
community(std::string_view base_url, std::string_view room, std::string_view pubkey_encoded);
|
||||
|
||||
// Takes a combined room URL (e.g. https://whatever.com/r/Room?public_key=01234....), either
|
||||
// new style (with /r/) or old style (without /r/). Note that the URL gets canonicalized so
|
||||
// the resulting `base_url()` and `room()` values may not be exactly equal to what is given.
|
||||
//
|
||||
// See also `parse_full_url` which does the same thing but returns it in pieces rather than
|
||||
// constructing a new `community` object.
|
||||
explicit community(std::string_view full_url);
|
||||
|
||||
// Replaces the baseurl/room/pubkey of this object from a URL. This parses the URL, then stores
|
||||
// the values as if passed to set_base_url/set_room/set_pubkey.
|
||||
//
|
||||
// The base URL will be normalized; the room name will be case-preserving (but see `set_room`
|
||||
// for info on limitations on "case-preserving", particularly for volatile configs); and the
|
||||
// embedded pubkey must be encoded in one of hex, base32z, or base64.
|
||||
void set_full_url(std::string_view full_url);
|
||||
|
||||
// Replaces the base_url of this object. Note that changing the URL and then giving it to `set`
|
||||
// will end up inserting a *new* record but not removing the *old* one (you need to erase first
|
||||
// to do that).
|
||||
void set_base_url(std::string_view new_url);
|
||||
|
||||
// Changes the room token. This stores (or updates) the name as given as the localized room,
|
||||
// and separately stores the normalized (lower-case) token. Note that the localized name does
|
||||
// not persist across a push or dump in some config contexts (such as volatile room info). If
|
||||
// the new room given here changes more than just case (i.e. if the normalized room token
|
||||
// changes) then a call to `set` will end up inserting a *new* record but not removing the *old*
|
||||
// one (you need to erase first to do that).
|
||||
void set_room(std::string_view room);
|
||||
|
||||
// Updates the pubkey of this community (typically this is not called directly but rather
|
||||
// via `set_server` or during construction). Throws std::invalid_argument if the given
|
||||
// pubkey does not look like a valid pubkey. The std::string_view version takes the pubkey
|
||||
// as any of hex/base64/base32z.
|
||||
//
|
||||
// NOTE: the pubkey of all communities with the same URLs are stored in common, so changing
|
||||
// one community pubkey (and storing) will affect all communities using the same community
|
||||
// base URL.
|
||||
void set_pubkey(ustring_view pubkey);
|
||||
void set_pubkey(std::string_view pubkey);
|
||||
|
||||
// Accesses the base url (i.e. not including room or pubkey). Always lower-case/normalized.
|
||||
const std::string& base_url() const { return base_url_; }
|
||||
|
||||
// Accesses the room token; this is case-preserving, where possible. In some contexts, however,
|
||||
// such as volatile info, the case is not preserved and this will always return the normalized
|
||||
// (lower-case) form rather than the preferred form.
|
||||
const std::string& room() const { return localized_room_ ? *localized_room_ : room_; }
|
||||
|
||||
// Accesses the normalized room token, i.e. always lower-case.
|
||||
const std::string& room_norm() const { return room_; }
|
||||
|
||||
const ustring& pubkey() const { return pubkey_; } // Accesses the server pubkey (32 bytes).
|
||||
std::string pubkey_hex() const; // Accesses the server pubkey as hex (64 hex digits).
|
||||
std::string pubkey_b32z() const; // Accesses the server pubkey as base32z (52 alphanumeric
|
||||
// digits)
|
||||
std::string pubkey_b64() const; // Accesses the server pubkey as unpadded base64 (43 from
|
||||
// alphanumeric, '+', and '/').
|
||||
|
||||
// Constructs and returns the full URL for this room. See below.
|
||||
std::string full_url() const;
|
||||
|
||||
// Constructs and returns the full URL for a given base, room, and pubkey. Currently this
|
||||
// returns it in a Session-compatibility form (https://server.com/RoomName?public_key=....), but
|
||||
// future versions are expected to change to use (https://server.com/r/RoomName?public_key=...),
|
||||
// which this library also accepts.
|
||||
static std::string full_url(
|
||||
std::string_view base_url, std::string_view room, ustring_view pubkey);
|
||||
|
||||
// Takes a base URL as input and returns it in canonical form. This involves doing things
|
||||
// like lower casing it and removing redundant ports (e.g. :80 when using http://). Throws
|
||||
// std::invalid_argument if given an invalid base URL.
|
||||
static std::string canonical_url(std::string_view url);
|
||||
|
||||
// Takes a room token and returns it in canonical form (i.e. lower-cased). Throws
|
||||
// std::invalid_argument if given an invalid room token (e.g. too long, or containing token
|
||||
// other than a-z, 0-9, -, _).
|
||||
static std::string canonical_room(std::string_view room);
|
||||
|
||||
// Same as above, but modifies the argument in-place instead of returning a modified
|
||||
// copy.
|
||||
static void canonicalize_url(std::string& url);
|
||||
static void canonicalize_room(std::string& room);
|
||||
|
||||
// Takes a full room URL, splits it up into canonical url (see above), room, and server
|
||||
// pubkey. We take both the deprecated form (e.g.
|
||||
// https://example.com/SomeRoom?public_key=...) and new form
|
||||
// (https://example.com/r/SomeRoom?public_key=...). The public_key is typically specified
|
||||
// in hex (64 digits), but we also accept base64 (43 chars or 44 with padding) and base32z
|
||||
// (52 chars) encodings (for slightly shorter URLs).
|
||||
//
|
||||
// The returned URL is normalized (lower-cased, and cleaned up).
|
||||
//
|
||||
// The returned room name is *not* normalized, that is, it preserve case.
|
||||
//
|
||||
// Throw std::invalid_argument if anything in the URL is unparseable or invalid.
|
||||
static std::tuple<std::string, std::string, ustring> parse_full_url(std::string_view full_url);
|
||||
|
||||
// Takes a full or partial room URL (partial here meaning missing the ?public_key=...) and
|
||||
// splits it up into canonical url, room, and (if present) pubkey.
|
||||
static std::tuple<std::string, std::string, std::optional<ustring>> parse_partial_url(
|
||||
std::string_view url);
|
||||
|
||||
protected:
|
||||
// The canonical base url and room (i.e. lower-cased, URL cleaned up):
|
||||
std::string base_url_, room_;
|
||||
// The localized token of this room, that is, with case preserved (so `room_` could be
|
||||
// `someroom` and this could `SomeRoom`). Omitted if not available.
|
||||
std::optional<std::string> localized_room_;
|
||||
// server pubkey
|
||||
ustring pubkey_;
|
||||
|
||||
// Construction without a pubkey for when pubkey isn't known yet but will be set shortly
|
||||
// after constructing (or when isn't needed, such as when deleting).
|
||||
community(std::string_view base_url, std::string_view room);
|
||||
};
|
||||
|
||||
struct comm_iterator_helper {
|
||||
|
||||
comm_iterator_helper(dict::const_iterator it_server, dict::const_iterator end_server) :
|
||||
it_server{std::move(it_server)}, end_server{std::move(end_server)} {}
|
||||
|
||||
std::optional<dict::const_iterator> it_server, end_server, it_room, end_room;
|
||||
|
||||
bool operator==(const comm_iterator_helper& other) const {
|
||||
return it_server == other.it_server && it_room == other.it_room;
|
||||
}
|
||||
|
||||
void next_server() {
|
||||
++*it_server;
|
||||
it_room.reset();
|
||||
end_room.reset();
|
||||
}
|
||||
|
||||
bool done() const { return !it_server || *it_server == *end_server; }
|
||||
|
||||
template <typename Comm, typename Any>
|
||||
bool load(std::shared_ptr<Any>& val) {
|
||||
while (it_server) {
|
||||
if (*it_server == *end_server) {
|
||||
it_server.reset();
|
||||
end_server.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& [base_url, server_info] = **it_server;
|
||||
auto* server_info_dict = std::get_if<dict>(&server_info);
|
||||
if (!server_info_dict) {
|
||||
next_server();
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string* pubkey_raw = nullptr;
|
||||
if (auto pubkey_it = server_info_dict->find("#"); pubkey_it != server_info_dict->end())
|
||||
if (auto* pk_sc = std::get_if<scalar>(&pubkey_it->second))
|
||||
pubkey_raw = std::get_if<std::string>(pk_sc);
|
||||
|
||||
if (!pubkey_raw) {
|
||||
next_server();
|
||||
continue;
|
||||
}
|
||||
|
||||
ustring_view pubkey{
|
||||
reinterpret_cast<const unsigned char*>(pubkey_raw->data()), pubkey_raw->size()};
|
||||
|
||||
if (!it_room) {
|
||||
if (auto rit = server_info_dict->find("R");
|
||||
rit != server_info_dict->end() && std::holds_alternative<dict>(rit->second)) {
|
||||
auto& rooms_dict = std::get<dict>(rit->second);
|
||||
it_room = rooms_dict.begin();
|
||||
end_room = rooms_dict.end();
|
||||
} else {
|
||||
next_server();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
while (it_room) {
|
||||
if (*it_room == *end_room) {
|
||||
it_room.reset();
|
||||
end_room.reset();
|
||||
break;
|
||||
}
|
||||
|
||||
auto& [room, data] = **it_room;
|
||||
auto* data_dict = std::get_if<dict>(&data);
|
||||
if (!data_dict) {
|
||||
++*it_room;
|
||||
continue;
|
||||
}
|
||||
|
||||
val = std::make_shared<Any>(Comm{});
|
||||
auto& og = std::get<Comm>(*val);
|
||||
try {
|
||||
og.set_base_url(base_url);
|
||||
og.set_room(room); // Will be replaced with "n" in the `.load` below
|
||||
og.set_pubkey(pubkey);
|
||||
og.load(*data_dict);
|
||||
} catch (const std::exception& e) {
|
||||
++*it_room;
|
||||
continue;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
++*it_server;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool advance() {
|
||||
if (it_room) {
|
||||
++*it_room;
|
||||
return true;
|
||||
}
|
||||
if (it_server) {
|
||||
++*it_server;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace session::config
|
|
@ -1,160 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include "base.h"
|
||||
#include "expiring.h"
|
||||
#include "notify.h"
|
||||
#include "profile_pic.h"
|
||||
#include "util.h"
|
||||
|
||||
// Maximum length of a contact name/nickname, in bytes (not including the null terminator).
|
||||
extern const size_t CONTACT_MAX_NAME_LENGTH;
|
||||
|
||||
typedef struct contacts_contact {
|
||||
char session_id[67]; // in hex; 66 hex chars + null terminator.
|
||||
|
||||
// These two will be 0-length strings when unset:
|
||||
char name[101];
|
||||
char nickname[101];
|
||||
user_profile_pic profile_pic;
|
||||
|
||||
bool approved;
|
||||
bool approved_me;
|
||||
bool blocked;
|
||||
|
||||
int priority;
|
||||
CONVO_NOTIFY_MODE notifications;
|
||||
int64_t mute_until;
|
||||
|
||||
CONVO_EXPIRATION_MODE exp_mode;
|
||||
int exp_seconds;
|
||||
|
||||
int64_t created; // unix timestamp (seconds)
|
||||
|
||||
} contacts_contact;
|
||||
|
||||
/// Constructs a contacts config object and sets a pointer to it in `conf`.
|
||||
///
|
||||
/// \param ed25519_secretkey must be the 32-byte secret key seed value. (You can also pass the
|
||||
/// pointer to the beginning of the 64-byte value libsodium calls the "secret key" as the first 32
|
||||
/// bytes of that are the seed). This field cannot be null.
|
||||
///
|
||||
/// \param dump - if non-NULL this restores the state from the dumped byte string produced by a past
|
||||
/// instantiation's call to `dump()`. To construct a new, empty object this should be NULL.
|
||||
///
|
||||
/// \param dumplen - the length of `dump` when restoring from a dump, or 0 when `dump` is NULL.
|
||||
///
|
||||
/// \param error - the pointer to a buffer in which we will write an error string if an error
|
||||
/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a
|
||||
/// buffer of at least 256 bytes.
|
||||
///
|
||||
/// Returns 0 on success; returns a non-zero error code and write the exception message as a
|
||||
/// C-string into `error` (if not NULL) on failure.
|
||||
///
|
||||
/// When done with the object the `config_object` must be destroyed by passing the pointer to
|
||||
/// config_free() (in `session/config/base.h`).
|
||||
int contacts_init(
|
||||
config_object** conf,
|
||||
const unsigned char* ed25519_secretkey,
|
||||
const unsigned char* dump,
|
||||
size_t dumplen,
|
||||
char* error) __attribute__((warn_unused_result));
|
||||
|
||||
/// Fills `contact` with the contact info given a session ID (specified as a null-terminated hex
|
||||
/// string), if the contact exists, and returns true. If the contact does not exist then `contact`
|
||||
/// is left unchanged and false is returned.
|
||||
bool contacts_get(config_object* conf, contacts_contact* contact, const char* session_id)
|
||||
__attribute__((warn_unused_result));
|
||||
|
||||
/// Same as the above except that when the contact does not exist, this sets all the contact fields
|
||||
/// to defaults and loads it with the given session_id.
|
||||
///
|
||||
/// Returns true as long as it is given a valid session_id. A false return is considered an error,
|
||||
/// and means the session_id was not a valid session_id.
|
||||
///
|
||||
/// This is the method that should usually be used to create or update a contact, followed by
|
||||
/// setting fields in the contact, and then giving it to contacts_set().
|
||||
bool contacts_get_or_construct(
|
||||
config_object* conf, contacts_contact* contact, const char* session_id)
|
||||
__attribute__((warn_unused_result));
|
||||
|
||||
/// Adds or updates a contact from the given contact info struct.
|
||||
void contacts_set(config_object* conf, const contacts_contact* contact);
|
||||
|
||||
// NB: wrappers for set_name, set_nickname, etc. C++ methods are deliberately omitted as they would
|
||||
// save very little in actual calling code. The procedure for updating a single field without them
|
||||
// is simple enough; for example to update `approved` and leave everything else unchanged:
|
||||
//
|
||||
// contacts_contact c;
|
||||
// if (contacts_get_or_construct(conf, &c, some_session_id)) {
|
||||
// const char* new_nickname = "Joe";
|
||||
// c.approved = new_nickname;
|
||||
// contacts_set_or_create(conf, &c);
|
||||
// } else {
|
||||
// // some_session_id was invalid!
|
||||
// }
|
||||
|
||||
/// Erases a contact from the contact list. session_id is in hex. Returns true if the contact was
|
||||
/// found and removed, false if the contact was not present. You must not call this during
|
||||
/// iteration; see details below.
|
||||
bool contacts_erase(config_object* conf, const char* session_id);
|
||||
|
||||
/// Returns the number of contacts.
|
||||
size_t contacts_size(const config_object* conf);
|
||||
|
||||
/// Functions for iterating through the entire contact list, in sorted order. Intended use is:
|
||||
///
|
||||
/// contacts_contact c;
|
||||
/// contacts_iterator *it = contacts_iterator_new(contacts);
|
||||
/// for (; !contacts_iterator_done(it, &c); contacts_iterator_advance(it)) {
|
||||
/// // c.session_id, c.nickname, etc. are loaded
|
||||
/// }
|
||||
/// contacts_iterator_free(it);
|
||||
///
|
||||
/// It is permitted to modify records (e.g. with a call to `contacts_set`) and add records while
|
||||
/// iterating.
|
||||
///
|
||||
/// If you need to remove while iterating then usage is slightly different: you must advance the
|
||||
/// iteration by calling either contacts_iterator_advance if not deleting, or
|
||||
/// contacts_iterator_erase to erase and advance. Usage looks like this:
|
||||
///
|
||||
/// contacts_contact c;
|
||||
/// contacts_iterator *it = contacts_iterator_new(contacts);
|
||||
/// while (!contacts_iterator_done(it, &c)) {
|
||||
/// // c.session_id, c.nickname, etc. are loaded
|
||||
///
|
||||
/// bool should_delete = /* ... */;
|
||||
///
|
||||
/// if (should_delete)
|
||||
/// contacts_iterator_erase(it);
|
||||
/// else
|
||||
/// contacts_iterator_advance(it);
|
||||
/// }
|
||||
/// contacts_iterator_free(it);
|
||||
///
|
||||
///
|
||||
|
||||
typedef struct contacts_iterator {
|
||||
void* _internals;
|
||||
} contacts_iterator;
|
||||
|
||||
// Starts a new iterator.
|
||||
contacts_iterator* contacts_iterator_new(const config_object* conf);
|
||||
// Frees an iterator once no longer needed.
|
||||
void contacts_iterator_free(contacts_iterator* it);
|
||||
|
||||
// Returns true if iteration has reached the end. Otherwise `c` is populated and false is returned.
|
||||
bool contacts_iterator_done(contacts_iterator* it, contacts_contact* c);
|
||||
|
||||
// Advances the iterator.
|
||||
void contacts_iterator_advance(contacts_iterator* it);
|
||||
|
||||
// Erases the current contact while advancing the iterator to the next contact in the iteration.
|
||||
void contacts_iterator_erase(config_object* conf, contacts_iterator* it);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
|
@ -1,231 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <session/config.hpp>
|
||||
|
||||
#include "base.hpp"
|
||||
#include "expiring.hpp"
|
||||
#include "namespaces.hpp"
|
||||
#include "notify.hpp"
|
||||
#include "profile_pic.hpp"
|
||||
|
||||
extern "C" struct contacts_contact;
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
namespace session::config {
|
||||
|
||||
/// keys used in this config, either currently or in the past (so that we don't reuse):
|
||||
///
|
||||
/// c - dict of contacts; within this dict each key is the session pubkey (binary, 33 bytes) and
|
||||
/// value is a dict containing keys:
|
||||
///
|
||||
/// n - contact name (string). This is always serialized, even if empty (but empty indicates
|
||||
/// no name) so that we always have at least one key set (required to keep the dict value
|
||||
/// alive as empty dicts get pruned).
|
||||
/// N - contact nickname (string)
|
||||
/// p - profile url (string)
|
||||
/// q - profile decryption key (binary)
|
||||
/// a - 1 if approved, omitted otherwise (int)
|
||||
/// A - 1 if remote has approved me, omitted otherwise (int)
|
||||
/// b - 1 if contact is blocked, omitted otherwise
|
||||
/// @ - notification setting (int). Omitted = use default setting; 1 = all; 2 = disabled.
|
||||
/// ! - mute timestamp: if this is set then notifications are to be muted until the given unix
|
||||
/// timestamp (seconds, not milliseconds).
|
||||
/// + - the conversation priority; -1 means hidden; omitted means not pinned; otherwise an
|
||||
/// integer value >0, where a higher priority means the conversation is meant to appear
|
||||
/// earlier in the pinned conversation list.
|
||||
/// e - Disappearing messages expiration type. Omitted if disappearing messages are not enabled
|
||||
/// for the conversation with this contact; 1 for delete-after-send, and 2 for
|
||||
/// delete-after-read.
|
||||
/// E - Disappearing message timer, in seconds. Omitted when `e` is omitted.
|
||||
/// j - Unix timestamp (seconds) when the contact was created ("j" to match user_groups
|
||||
/// equivalent "j"oined field). Omitted if 0.
|
||||
|
||||
/// Struct containing contact info.
|
||||
struct contact_info {
|
||||
static constexpr size_t MAX_NAME_LENGTH = 100;
|
||||
|
||||
std::string session_id; // in hex
|
||||
std::string name;
|
||||
std::string nickname;
|
||||
profile_pic profile_picture;
|
||||
bool approved = false;
|
||||
bool approved_me = false;
|
||||
bool blocked = false;
|
||||
int priority = 0; // If >0 then this message is pinned; higher values mean higher priority
|
||||
// (i.e. pinned earlier in the pinned list). If negative then this
|
||||
// conversation is hidden. Otherwise (0) this is a regular, unpinned
|
||||
// conversation.
|
||||
notify_mode notifications = notify_mode::defaulted;
|
||||
int64_t mute_until = 0; // If non-zero, disable notifications until the given unix timestamp
|
||||
// (overriding whatever the current `notifications` value is until the
|
||||
// timestamp expires).
|
||||
expiration_mode exp_mode = expiration_mode::none; // The expiry time; none if not expiring.
|
||||
std::chrono::seconds exp_timer{0}; // The expiration timer (in seconds)
|
||||
int64_t created = 0; // Unix timestamp when this contact was added
|
||||
|
||||
explicit contact_info(std::string sid);
|
||||
|
||||
// Internal ctor/method for C API implementations:
|
||||
contact_info(const struct contacts_contact& c); // From c struct
|
||||
void into(contacts_contact& c) const; // Into c struct
|
||||
|
||||
// Sets a name or nickname; this is exactly the same as assigning to .name/.nickname directly,
|
||||
// except that we throw an exception if the given name is longer than MAX_NAME_LENGTH.
|
||||
void set_name(std::string name);
|
||||
void set_nickname(std::string nickname);
|
||||
|
||||
private:
|
||||
friend class Contacts;
|
||||
void load(const dict& info_dict);
|
||||
};
|
||||
|
||||
class Contacts : public ConfigBase {
|
||||
|
||||
public:
|
||||
// No default constructor
|
||||
Contacts() = delete;
|
||||
|
||||
/// Constructs a contact list from existing data (stored from `dump()`) and the user's secret
|
||||
/// key for generating the data encryption key. To construct a blank list (i.e. with no
|
||||
/// pre-existing dumped data to load) pass `std::nullopt` as the second argument.
|
||||
///
|
||||
/// \param ed25519_secretkey - contains the libsodium secret key used to encrypt/decrypt the
|
||||
/// data when pushing/pulling from the swarm. This can either be the full 64-byte value (which
|
||||
/// is technically the 32-byte seed followed by the 32-byte pubkey), or just the 32-byte seed of
|
||||
/// the secret key.
|
||||
///
|
||||
/// \param dumped - either `std::nullopt` to construct a new, empty object; or binary state data
|
||||
/// that was previously dumped from an instance of this class by calling `dump()`.
|
||||
Contacts(ustring_view ed25519_secretkey, std::optional<ustring_view> dumped);
|
||||
|
||||
Namespace storage_namespace() const override { return Namespace::Contacts; }
|
||||
|
||||
const char* encryption_domain() const override { return "Contacts"; }
|
||||
|
||||
/// Looks up and returns a contact by session ID (hex). Returns nullopt if the session ID was
|
||||
/// not found, otherwise returns a filled out `contact_info`.
|
||||
std::optional<contact_info> get(std::string_view pubkey_hex) const;
|
||||
|
||||
/// Similar to get(), but if the session ID does not exist this returns a filled-out
|
||||
/// contact_info containing the session_id (all other fields will be empty/defaulted). This is
|
||||
/// intended to be combined with `set` to set-or-create a record.
|
||||
///
|
||||
/// NB: calling this does *not* add the session id to the contact list when called: that
|
||||
/// requires also calling `set` with this value.
|
||||
contact_info get_or_construct(std::string_view pubkey_hex) const;
|
||||
|
||||
/// Sets or updates multiple contact info values at once with the given info. The usual use is
|
||||
/// to access the current info, change anything desired, then pass it back into set_contact,
|
||||
/// e.g.:
|
||||
///
|
||||
/// auto c = contacts.get_or_construct(pubkey);
|
||||
/// c.name = "Session User 42";
|
||||
/// c.nickname = "BFF";
|
||||
/// contacts.set(c);
|
||||
void set(const contact_info& contact);
|
||||
|
||||
/// Alternative to `set()` for setting a single field. (If setting multiple fields at once you
|
||||
/// should use `set()` instead).
|
||||
void set_name(std::string_view session_id, std::string name);
|
||||
void set_nickname(std::string_view session_id, std::string nickname);
|
||||
void set_profile_pic(std::string_view session_id, profile_pic pic);
|
||||
void set_approved(std::string_view session_id, bool approved);
|
||||
void set_approved_me(std::string_view session_id, bool approved_me);
|
||||
void set_blocked(std::string_view session_id, bool blocked);
|
||||
void set_priority(std::string_view session_id, int priority);
|
||||
void set_notifications(std::string_view session_id, notify_mode notifications);
|
||||
void set_expiry(
|
||||
std::string_view session_id,
|
||||
expiration_mode exp_mode,
|
||||
std::chrono::seconds expiration_timer = 0min);
|
||||
void set_created(std::string_view session_id, int64_t timestamp);
|
||||
|
||||
/// Removes a contact, if present. Returns true if it was found and removed, false otherwise.
|
||||
/// Note that this removes all fields related to a contact, even fields we do not know about.
|
||||
bool erase(std::string_view session_id);
|
||||
|
||||
struct iterator;
|
||||
|
||||
/// This works like erase, but takes an iterator to the contact to remove. The element is
|
||||
/// removed and the iterator to the next element after the removed one is returned. This is
|
||||
/// intended for use where elements are to be removed during iteration: see below for an
|
||||
/// example.
|
||||
iterator erase(iterator it);
|
||||
|
||||
/// Returns the number of contacts.
|
||||
size_t size() const;
|
||||
|
||||
/// Returns true if the contact list is empty.
|
||||
bool empty() const { return size() == 0; }
|
||||
|
||||
/// Iterators for iterating through all contacts. Typically you access this implicit via a for
|
||||
/// loop over the `Contacts` object:
|
||||
///
|
||||
/// for (auto& contact : contacts) {
|
||||
/// // use contact.session_id, contact.name, etc.
|
||||
/// }
|
||||
///
|
||||
/// This iterates in sorted order through the session_ids.
|
||||
///
|
||||
/// It is permitted to modify and add records while iterating (e.g. by modifying `contact` and
|
||||
/// then calling set()).
|
||||
///
|
||||
/// If you need to erase the current contact during iteration then care is required: you need to
|
||||
/// advance the iterator via the iterator version of erase when erasing an element rather than
|
||||
/// incrementing it regularly. For example:
|
||||
///
|
||||
/// for (auto it = contacts.begin(); it != contacts.end(); ) {
|
||||
/// if (should_remove(*it))
|
||||
/// it = contacts.erase(it);
|
||||
/// else
|
||||
/// ++it;
|
||||
/// }
|
||||
///
|
||||
/// Alternatively, you can use the first version with two loops: the first loop through all
|
||||
/// contacts doesn't erase but just builds a vector of IDs to erase, then the second loops
|
||||
/// through that vector calling `erase()` for each one.
|
||||
///
|
||||
iterator begin() const { return iterator{data["c"].dict()}; }
|
||||
iterator end() const { return iterator{nullptr}; }
|
||||
|
||||
using iterator_category = std::input_iterator_tag;
|
||||
using value_type = contact_info;
|
||||
using reference = value_type&;
|
||||
using pointer = value_type*;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
|
||||
struct iterator {
|
||||
private:
|
||||
std::shared_ptr<contact_info> _val;
|
||||
dict::const_iterator _it;
|
||||
const dict* _contacts;
|
||||
void _load_info();
|
||||
iterator(const dict* contacts) : _contacts{contacts} {
|
||||
if (_contacts) {
|
||||
_it = _contacts->begin();
|
||||
_load_info();
|
||||
}
|
||||
}
|
||||
friend class Contacts;
|
||||
|
||||
public:
|
||||
bool operator==(const iterator& other) const;
|
||||
bool operator!=(const iterator& other) const { return !(*this == other); }
|
||||
bool done() const; // Equivalent to comparing against the end iterator
|
||||
contact_info& operator*() const { return *_val; }
|
||||
contact_info* operator->() const { return _val.get(); }
|
||||
iterator& operator++();
|
||||
iterator operator++(int) {
|
||||
auto copy{*this};
|
||||
++*this;
|
||||
return copy;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
} // namespace session::config
|
|
@ -1,229 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include "base.h"
|
||||
#include "profile_pic.h"
|
||||
|
||||
typedef struct convo_info_volatile_1to1 {
|
||||
char session_id[67]; // in hex; 66 hex chars + null terminator.
|
||||
|
||||
int64_t last_read; // milliseconds since unix epoch
|
||||
bool unread; // true if the conversation is explicitly marked unread
|
||||
} convo_info_volatile_1to1;
|
||||
|
||||
typedef struct convo_info_volatile_community {
|
||||
char base_url[268]; // null-terminated (max length 267), normalized (i.e. always lower-case,
|
||||
// only has port if non-default, has trailing / removed)
|
||||
char room[65]; // null-terminated (max length 64), normalized (always lower-case)
|
||||
unsigned char pubkey[32]; // 32 bytes (not terminated, can contain nulls)
|
||||
|
||||
int64_t last_read; // ms since unix epoch
|
||||
bool unread; // true if marked unread
|
||||
} convo_info_volatile_community;
|
||||
|
||||
typedef struct convo_info_volatile_legacy_group {
|
||||
char group_id[67]; // in hex; 66 hex chars + null terminator. Looks just like a Session ID,
|
||||
// though isn't really one.
|
||||
|
||||
int64_t last_read; // ms since unix epoch
|
||||
bool unread; // true if marked unread
|
||||
} convo_info_volatile_legacy_group;
|
||||
|
||||
/// Constructs a conversations config object and sets a pointer to it in `conf`.
|
||||
///
|
||||
/// \param ed25519_secretkey must be the 32-byte secret key seed value. (You can also pass the
|
||||
/// pointer to the beginning of the 64-byte value libsodium calls the "secret key" as the first 32
|
||||
/// bytes of that are the seed). This field cannot be null.
|
||||
///
|
||||
/// \param dump - if non-NULL this restores the state from the dumped byte string produced by a past
|
||||
/// instantiation's call to `dump()`. To construct a new, empty object this should be NULL.
|
||||
///
|
||||
/// \param dumplen - the length of `dump` when restoring from a dump, or 0 when `dump` is NULL.
|
||||
///
|
||||
/// \param error - the pointer to a buffer in which we will write an error string if an error
|
||||
/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a
|
||||
/// buffer of at least 256 bytes.
|
||||
///
|
||||
/// Returns 0 on success; returns a non-zero error code and write the exception message as a
|
||||
/// C-string into `error` (if not NULL) on failure.
|
||||
///
|
||||
/// When done with the object the `config_object` must be destroyed by passing the pointer to
|
||||
/// config_free() (in `session/config/base.h`).
|
||||
int convo_info_volatile_init(
|
||||
config_object** conf,
|
||||
const unsigned char* ed25519_secretkey,
|
||||
const unsigned char* dump,
|
||||
size_t dumplen,
|
||||
char* error) __attribute__((warn_unused_result));
|
||||
|
||||
/// Fills `convo` with the conversation info given a session ID (specified as a null-terminated hex
|
||||
/// string), if the conversation exists, and returns true. If the conversation does not exist then
|
||||
/// `convo` is left unchanged and false is returned. If an error occurs, false is returned and
|
||||
/// `conf->last_error` will be set to non-NULL containing the error string (if no error occurs, such
|
||||
/// as in the case where the conversation merely doesn't exist, `last_error` will be set to NULL).
|
||||
bool convo_info_volatile_get_1to1(
|
||||
config_object* conf, convo_info_volatile_1to1* convo, const char* session_id)
|
||||
__attribute__((warn_unused_result));
|
||||
|
||||
/// Same as the above except that when the conversation does not exist, this sets all the convo
|
||||
/// fields to defaults and loads it with the given session_id.
|
||||
///
|
||||
/// Returns true as long as it is given a valid session_id. A false return is considered an error,
|
||||
/// and means the session_id was not a valid session_id. In such a case `conf->last_error` will be
|
||||
/// set to an error string.
|
||||
///
|
||||
/// This is the method that should usually be used to create or update a conversation, followed by
|
||||
/// setting fields in the convo, and then giving it to convo_info_volatile_set().
|
||||
bool convo_info_volatile_get_or_construct_1to1(
|
||||
config_object* conf, convo_info_volatile_1to1* convo, const char* session_id)
|
||||
__attribute__((warn_unused_result));
|
||||
|
||||
/// community versions of the 1-to-1 functions:
|
||||
///
|
||||
/// Gets a community convo info. `base_url` and `room` are null-terminated c strings; pubkey is
|
||||
/// 32 bytes. base_url and room will always be lower-cased (if not already).
|
||||
///
|
||||
/// Error handling works the same as the 1-to-1 version.
|
||||
bool convo_info_volatile_get_community(
|
||||
config_object* conf,
|
||||
convo_info_volatile_community* comm,
|
||||
const char* base_url,
|
||||
const char* room) __attribute__((warn_unused_result));
|
||||
bool convo_info_volatile_get_or_construct_community(
|
||||
config_object* conf,
|
||||
convo_info_volatile_community* convo,
|
||||
const char* base_url,
|
||||
const char* room,
|
||||
unsigned const char* pubkey) __attribute__((warn_unused_result));
|
||||
|
||||
/// Fills `convo` with the conversation info given a legacy group ID (specified as a null-terminated
|
||||
/// hex string), if the conversation exists, and returns true. If the conversation does not exist
|
||||
/// then `convo` is left unchanged and false is returned. On error, false is returned and the error
|
||||
/// is set in conf->last_error (on non-error, last_error is cleared).
|
||||
bool convo_info_volatile_get_legacy_group(
|
||||
config_object* conf, convo_info_volatile_legacy_group* convo, const char* id)
|
||||
__attribute__((warn_unused_result));
|
||||
|
||||
/// Same as the above except that when the conversation does not exist, this sets all the convo
|
||||
/// fields to defaults and loads it with the given id.
|
||||
///
|
||||
/// Returns true as long as it is given a valid legacy group id (i.e. same format as a session id).
|
||||
/// A false return is considered an error, and means the id was not a valid session id; an error
|
||||
/// string will be set in `conf->last_error`.
|
||||
///
|
||||
/// This is the method that should usually be used to create or update a conversation, followed by
|
||||
/// setting fields in the convo, and then giving it to convo_info_volatile_set().
|
||||
bool convo_info_volatile_get_or_construct_legacy_group(
|
||||
config_object* conf, convo_info_volatile_legacy_group* convo, const char* id)
|
||||
__attribute__((warn_unused_result));
|
||||
|
||||
/// Adds or updates a conversation from the given convo info
|
||||
void convo_info_volatile_set_1to1(config_object* conf, const convo_info_volatile_1to1* convo);
|
||||
void convo_info_volatile_set_community(
|
||||
config_object* conf, const convo_info_volatile_community* convo);
|
||||
void convo_info_volatile_set_legacy_group(
|
||||
config_object* conf, const convo_info_volatile_legacy_group* convo);
|
||||
|
||||
/// Erases a conversation from the conversation list. Returns true if the conversation was found
|
||||
/// and removed, false if the conversation was not present. You must not call this during
|
||||
/// iteration; see details below.
|
||||
bool convo_info_volatile_erase_1to1(config_object* conf, const char* session_id);
|
||||
bool convo_info_volatile_erase_community(
|
||||
config_object* conf, const char* base_url, const char* room);
|
||||
bool convo_info_volatile_erase_legacy_group(config_object* conf, const char* group_id);
|
||||
|
||||
/// Returns the number of conversations.
|
||||
size_t convo_info_volatile_size(const config_object* conf);
|
||||
/// Returns the number of conversations of the specific type.
|
||||
size_t convo_info_volatile_size_1to1(const config_object* conf);
|
||||
size_t convo_info_volatile_size_communities(const config_object* conf);
|
||||
size_t convo_info_volatile_size_legacy_groups(const config_object* conf);
|
||||
|
||||
/// Functions for iterating through the entire conversation list. Intended use is:
|
||||
///
|
||||
/// convo_info_volatile_1to1 c1;
|
||||
/// convo_info_volatile_community c2;
|
||||
/// convo_info_volatile_legacy_group c3;
|
||||
/// convo_info_volatile_iterator *it = convo_info_volatile_iterator_new(my_convos);
|
||||
/// for (; !convo_info_volatile_iterator_done(it); convo_info_volatile_iterator_advance(it)) {
|
||||
/// if (convo_info_volatile_it_is_1to1(it, &c1)) {
|
||||
/// // use c1.whatever
|
||||
/// } else if (convo_info_volatile_it_is_community(it, &c2)) {
|
||||
/// // use c2.whatever
|
||||
/// } else if (convo_info_volatile_it_is_legacy_group(it, &c3)) {
|
||||
/// // use c3.whatever
|
||||
/// }
|
||||
/// }
|
||||
/// convo_info_volatile_iterator_free(it);
|
||||
///
|
||||
/// It is permitted to modify records (e.g. with a call to one of the `convo_info_volatile_set_*`
|
||||
/// functions) and add records while iterating.
|
||||
///
|
||||
/// If you need to remove while iterating then usage is slightly different: you must advance the
|
||||
/// iteration by calling either convo_info_volatile_iterator_advance if not deleting, or
|
||||
/// convo_info_volatile_iterator_erase to erase and advance. Usage looks like this:
|
||||
///
|
||||
/// convo_info_volatile_1to1 c1;
|
||||
/// convo_info_volatile_iterator *it = convo_info_volatile_iterator_new(my_convos);
|
||||
/// while (!convo_info_volatile_iterator_done(it)) {
|
||||
/// if (convo_it_is_1to1(it, &c1)) {
|
||||
/// bool should_delete = /* ... */;
|
||||
/// if (should_delete)
|
||||
/// convo_info_volatile_iterator_erase(it);
|
||||
/// else
|
||||
/// convo_info_volatile_iterator_advance(it);
|
||||
/// } else {
|
||||
/// convo_info_volatile_iterator_advance(it);
|
||||
/// }
|
||||
/// }
|
||||
/// convo_info_volatile_iterator_free(it);
|
||||
///
|
||||
|
||||
typedef struct convo_info_volatile_iterator convo_info_volatile_iterator;
|
||||
|
||||
// Starts a new iterator that iterates over all conversations.
|
||||
convo_info_volatile_iterator* convo_info_volatile_iterator_new(const config_object* conf);
|
||||
|
||||
// The same as `convo_info_volatile_iterator_new` except that this iterates *only* over one type of
|
||||
// conversation. You still need to use `convo_info_volatile_it_is_1to1` (or the alternatives) to
|
||||
// load the data in each pass of the loop. (You can, however, safely ignore the bool return value
|
||||
// of the `it_is_whatever` function: it will always be true for the particular type being iterated
|
||||
// over).
|
||||
convo_info_volatile_iterator* convo_info_volatile_iterator_new_1to1(const config_object* conf);
|
||||
convo_info_volatile_iterator* convo_info_volatile_iterator_new_communities(
|
||||
const config_object* conf);
|
||||
convo_info_volatile_iterator* convo_info_volatile_iterator_new_legacy_groups(
|
||||
const config_object* conf);
|
||||
|
||||
// Frees an iterator once no longer needed.
|
||||
void convo_info_volatile_iterator_free(convo_info_volatile_iterator* it);
|
||||
|
||||
// Returns true if iteration has reached the end.
|
||||
bool convo_info_volatile_iterator_done(convo_info_volatile_iterator* it);
|
||||
|
||||
// Advances the iterator.
|
||||
void convo_info_volatile_iterator_advance(convo_info_volatile_iterator* it);
|
||||
|
||||
// If the current iterator record is a 1-to-1 conversation this sets the details into `c` and
|
||||
// returns true. Otherwise it returns false.
|
||||
bool convo_info_volatile_it_is_1to1(convo_info_volatile_iterator* it, convo_info_volatile_1to1* c);
|
||||
|
||||
// If the current iterator record is a community conversation this sets the details into `c` and
|
||||
// returns true. Otherwise it returns false.
|
||||
bool convo_info_volatile_it_is_community(
|
||||
convo_info_volatile_iterator* it, convo_info_volatile_community* c);
|
||||
|
||||
// If the current iterator record is a legacy group conversation this sets the details into `c` and
|
||||
// returns true. Otherwise it returns false.
|
||||
bool convo_info_volatile_it_is_legacy_group(
|
||||
convo_info_volatile_iterator* it, convo_info_volatile_legacy_group* c);
|
||||
|
||||
// Erases the current convo while advancing the iterator to the next convo in the iteration.
|
||||
void convo_info_volatile_iterator_erase(config_object* conf, convo_info_volatile_iterator* it);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
|
@ -1,347 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <session/config.hpp>
|
||||
|
||||
#include "base.hpp"
|
||||
#include "community.hpp"
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
extern "C" {
|
||||
struct convo_info_volatile_1to1;
|
||||
struct convo_info_volatile_community;
|
||||
struct convo_info_volatile_legacy_group;
|
||||
}
|
||||
|
||||
namespace session::config {
|
||||
|
||||
class ConvoInfoVolatile;
|
||||
|
||||
/// keys used in this config, either currently or in the past (so that we don't reuse):
|
||||
///
|
||||
/// Note that this is a high-frequency object, intended only for properties that change frequently (
|
||||
/// (currently just the read timestamp for each conversation).
|
||||
///
|
||||
/// 1 - dict of one-to-one conversations. Each key is the Session ID of the contact (in hex).
|
||||
/// Values are dicts with keys:
|
||||
/// r - the unix timestamp (in integer milliseconds) of the last-read message. Always
|
||||
/// included, but will be 0 if no messages are read.
|
||||
/// u - will be present and set to 1 if this conversation is specifically marked unread.
|
||||
///
|
||||
/// o - community conversations. This is a nested dict where the outer keys are the BASE_URL of the
|
||||
/// community and the outer value is a dict containing:
|
||||
/// - `#` -- the 32-byte server pubkey
|
||||
/// - `R` -- dict of rooms on the server; each key is the lower-case room name, value is a dict
|
||||
/// containing keys:
|
||||
/// r - the unix timestamp (in integer milliseconds) of the last-read message. Always
|
||||
/// included, but will be 0 if no messages are read.
|
||||
/// u - will be present and set to 1 if this conversation is specifically marked unread.
|
||||
///
|
||||
/// C - legacy group conversations (aka closed groups). The key is the group identifier (which
|
||||
/// looks indistinguishable from a Session ID, but isn't really a proper Session ID). Values
|
||||
/// are dicts with keys:
|
||||
/// r - the unix timestamp (integer milliseconds) of the last-read message. Always included,
|
||||
/// but will be 0 if no messages are read.
|
||||
/// u - will be present and set to 1 if this conversation is specifically marked unread.
|
||||
///
|
||||
/// c - reserved for future tracking of new group conversations.
|
||||
|
||||
namespace convo {
|
||||
|
||||
struct base {
|
||||
int64_t last_read = 0;
|
||||
bool unread = false;
|
||||
|
||||
protected:
|
||||
void load(const dict& info_dict);
|
||||
};
|
||||
|
||||
struct one_to_one : base {
|
||||
std::string session_id; // in hex
|
||||
|
||||
// Constructs an empty one_to_one from a session_id. Session ID can be either bytes (33) or
|
||||
// hex (66).
|
||||
explicit one_to_one(std::string&& session_id);
|
||||
explicit one_to_one(std::string_view session_id);
|
||||
|
||||
// Internal ctor/method for C API implementations:
|
||||
one_to_one(const struct convo_info_volatile_1to1& c); // From c struct
|
||||
void into(convo_info_volatile_1to1& c) const; // Into c struct
|
||||
|
||||
friend class session::config::ConvoInfoVolatile;
|
||||
};
|
||||
|
||||
struct community : config::community, base {
|
||||
|
||||
using config::community::community;
|
||||
|
||||
// Internal ctor/method for C API implementations:
|
||||
community(const convo_info_volatile_community& c); // From c struct
|
||||
void into(convo_info_volatile_community& c) const; // Into c struct
|
||||
|
||||
friend class session::config::ConvoInfoVolatile;
|
||||
friend struct session::config::comm_iterator_helper;
|
||||
};
|
||||
|
||||
struct legacy_group : base {
|
||||
std::string id; // in hex, indistinguishable from a Session ID
|
||||
|
||||
// Constructs an empty legacy_group from a quasi-session_id
|
||||
explicit legacy_group(std::string&& group_id);
|
||||
explicit legacy_group(std::string_view group_id);
|
||||
|
||||
// Internal ctor/method for C API implementations:
|
||||
legacy_group(const struct convo_info_volatile_legacy_group& c); // From c struct
|
||||
void into(convo_info_volatile_legacy_group& c) const; // Into c struct
|
||||
|
||||
private:
|
||||
friend class session::config::ConvoInfoVolatile;
|
||||
};
|
||||
|
||||
using any = std::variant<one_to_one, community, legacy_group>;
|
||||
} // namespace convo
|
||||
|
||||
class ConvoInfoVolatile : public ConfigBase {
|
||||
|
||||
public:
|
||||
// No default constructor
|
||||
ConvoInfoVolatile() = delete;
|
||||
|
||||
/// Constructs a conversation list from existing data (stored from `dump()`) and the user's
|
||||
/// secret key for generating the data encryption key. To construct a blank list (i.e. with no
|
||||
/// pre-existing dumped data to load) pass `std::nullopt` as the second argument.
|
||||
///
|
||||
/// \param ed25519_secretkey - contains the libsodium secret key used to encrypt/decrypt the
|
||||
/// data when pushing/pulling from the swarm. This can either be the full 64-byte value (which
|
||||
/// is technically the 32-byte seed followed by the 32-byte pubkey), or just the 32-byte seed of
|
||||
/// the secret key.
|
||||
///
|
||||
/// \param dumped - either `std::nullopt` to construct a new, empty object; or binary state data
|
||||
/// that was previously dumped from an instance of this class by calling `dump()`.
|
||||
ConvoInfoVolatile(ustring_view ed25519_secretkey, std::optional<ustring_view> dumped);
|
||||
|
||||
Namespace storage_namespace() const override { return Namespace::ConvoInfoVolatile; }
|
||||
|
||||
const char* encryption_domain() const override { return "ConvoInfoVolatile"; }
|
||||
|
||||
/// Our pruning ages. We ignore added conversations that are more than PRUNE_LOW before now,
|
||||
/// and we active remove (when doing a new push) any conversations that are more than PRUNE_HIGH
|
||||
/// before now. Clients can mostly ignore these and just add all conversations; the class just
|
||||
/// transparently ignores (or removes) pruned values.
|
||||
static constexpr auto PRUNE_LOW = 30 * 24h;
|
||||
static constexpr auto PRUNE_HIGH = 45 * 24h;
|
||||
|
||||
/// Overrides push() to prune stale last-read values before we do the push.
|
||||
std::tuple<seqno_t, ustring, std::vector<std::string>> push() override;
|
||||
|
||||
/// Looks up and returns a contact by session ID (hex). Returns nullopt if the session ID was
|
||||
/// not found, otherwise returns a filled out `convo::one_to_one`.
|
||||
std::optional<convo::one_to_one> get_1to1(std::string_view session_id) const;
|
||||
|
||||
/// Looks up and returns a community conversation. Takes the base URL and room name (case
|
||||
/// insensitive). Retuns nullopt if the community was not found, otherwise a filled out
|
||||
/// `convo::community`.
|
||||
std::optional<convo::community> get_community(
|
||||
std::string_view base_url, std::string_view room) const;
|
||||
|
||||
/// Shortcut for calling community::parse_partial_url then calling the above with the base url
|
||||
/// and room. The URL is not required to contain the pubkey (if present it will be ignored).
|
||||
std::optional<convo::community> get_community(std::string_view partial_url) const;
|
||||
|
||||
/// Looks up and returns a legacy group conversation by ID. The ID looks like a hex Session ID,
|
||||
/// but isn't really a Session ID. Returns nullopt if there is no record of the group
|
||||
/// conversation.
|
||||
std::optional<convo::legacy_group> get_legacy_group(std::string_view pubkey_hex) const;
|
||||
|
||||
/// These are the same as the above methods (without "_or_construct" in the name), except that
|
||||
/// when the conversation doesn't exist a new one is created, prefilled with the pubkey/url/etc.
|
||||
convo::one_to_one get_or_construct_1to1(std::string_view session_id) const;
|
||||
convo::legacy_group get_or_construct_legacy_group(std::string_view pubkey_hex) const;
|
||||
|
||||
/// This is similar to get_community, except that it also takes the pubkey; the community is
|
||||
/// looked up by the url & room; if not found, it is constructed using room, url, and pubkey; if
|
||||
/// it *is* found, then it will always have the *input* pubkey, not the stored pubkey
|
||||
/// (effectively the provided pubkey replaces the stored one in the returned object; this is not
|
||||
/// applied to storage, however, unless/until the instance is given to `set()`).
|
||||
///
|
||||
/// Note, however, that when modifying an object like this the update is *only* applied to the
|
||||
/// returned object; like other fields, it is not updated in the internal state unless/until
|
||||
/// that community instance is passed to `set()`.
|
||||
convo::community get_or_construct_community(
|
||||
std::string_view base_url, std::string_view room, std::string_view pubkey_hex) const;
|
||||
convo::community get_or_construct_community(
|
||||
std::string_view base_url, std::string_view room, ustring_view pubkey) const;
|
||||
|
||||
// Shortcut for calling community::parse_full_url then calling the above
|
||||
convo::community get_or_construct_community(std::string_view full_url) const;
|
||||
|
||||
/// Inserts or replaces existing conversation info. For example, to update a 1-to-1
|
||||
/// conversation last read time you would do:
|
||||
///
|
||||
/// auto info = conversations.get_or_construct_1to1(some_session_id);
|
||||
/// info.last_read = new_unix_timestamp;
|
||||
/// conversations.set(info);
|
||||
///
|
||||
void set(const convo::one_to_one& c);
|
||||
void set(const convo::legacy_group& c);
|
||||
void set(const convo::community& c);
|
||||
|
||||
void set(const convo::any& c); // Variant which can be any of the above
|
||||
|
||||
protected:
|
||||
void set_base(const convo::base& c, DictFieldProxy& info);
|
||||
|
||||
// Drills into the nested dicts to access community details; if the second argument is
|
||||
// non-nullptr then it will be set to the community's pubkey, if it exists.
|
||||
DictFieldProxy community_field(
|
||||
const convo::community& og, ustring_view* get_pubkey = nullptr) const;
|
||||
|
||||
public:
|
||||
/// Removes a one-to-one conversation. Returns true if found and removed, false if not present.
|
||||
bool erase_1to1(std::string_view pubkey);
|
||||
|
||||
/// Removes a community conversation record. Returns true if found and removed, false if not
|
||||
/// present. Arguments are the same as `get_community`.
|
||||
bool erase_community(std::string_view base_url, std::string_view room);
|
||||
|
||||
/// Removes a legacy group conversation. Returns true if found and removed, false if not
|
||||
/// present.
|
||||
bool erase_legacy_group(std::string_view pubkey_hex);
|
||||
|
||||
/// Removes a conversation taking the convo::whatever record (rather than the pubkey/url).
|
||||
bool erase(const convo::one_to_one& c);
|
||||
bool erase(const convo::community& c);
|
||||
bool erase(const convo::legacy_group& c);
|
||||
|
||||
bool erase(const convo::any& c); // Variant of any of them
|
||||
|
||||
struct iterator;
|
||||
|
||||
/// This works like erase, but takes an iterator to the conversation to remove. The element is
|
||||
/// removed and the iterator to the next element after the removed one is returned. This is
|
||||
/// intended for use where elements are to be removed during iteration: see below for an
|
||||
/// example.
|
||||
iterator erase(iterator it);
|
||||
|
||||
/// Returns the number of conversations (of any type).
|
||||
size_t size() const;
|
||||
|
||||
/// Returns the number of 1-to-1, community, and legacy group conversations, respectively.
|
||||
size_t size_1to1() const;
|
||||
size_t size_communities() const;
|
||||
size_t size_legacy_groups() const;
|
||||
|
||||
/// Returns true if the conversation list is empty.
|
||||
bool empty() const { return size() == 0; }
|
||||
|
||||
/// Iterators for iterating through all conversations. Typically you access this implicit via a
|
||||
/// for loop over the `ConvoInfoVolatile` object:
|
||||
///
|
||||
/// for (auto& convo : conversations) {
|
||||
/// if (auto* dm = std::get_if<convo::one_to_one>(&convo)) {
|
||||
/// // use dm->session_id, dm->last_read, etc.
|
||||
/// } else if (auto* og = std::get_if<convo::community>(&convo)) {
|
||||
/// // use og->base_url, og->room, om->last_read, etc.
|
||||
/// } else if (auto* lcg = std::get_if<convo::legacy_group>(&convo)) {
|
||||
/// // use lcg->id, lcg->last_read
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// This iterates through all conversations in sorted order (sorted first by convo type, then by
|
||||
/// id within the type).
|
||||
///
|
||||
/// It is permitted to modify and add records while iterating (e.g. by modifying one of the
|
||||
/// `dm`/`og`/`lcg` and then calling set()).
|
||||
///
|
||||
/// If you need to erase the current conversation during iteration then care is required: you
|
||||
/// need to advance the iterator via the iterator version of erase when erasing an element
|
||||
/// rather than incrementing it regularly. For example:
|
||||
///
|
||||
/// for (auto it = conversations.begin(); it != conversations.end(); ) {
|
||||
/// if (should_remove(*it))
|
||||
/// it = converations.erase(it);
|
||||
/// else
|
||||
/// ++it;
|
||||
/// }
|
||||
///
|
||||
/// Alternatively, you can use the first version with two loops: the first loop through all
|
||||
/// converations doesn't erase but just builds a vector of IDs to erase, then the second loops
|
||||
/// through that vector calling `erase_1to1()`/`erase_community()`/`erase_legacy_group()` for
|
||||
/// each one.
|
||||
///
|
||||
iterator begin() const { return iterator{data}; }
|
||||
iterator end() const { return iterator{}; }
|
||||
|
||||
template <typename ConvoType>
|
||||
struct subtype_iterator;
|
||||
|
||||
/// Returns an iterator that iterates only through one type of conversations
|
||||
subtype_iterator<convo::one_to_one> begin_1to1() const { return {data}; }
|
||||
subtype_iterator<convo::community> begin_communities() const { return {data}; }
|
||||
subtype_iterator<convo::legacy_group> begin_legacy_groups() const { return {data}; }
|
||||
|
||||
using iterator_category = std::input_iterator_tag;
|
||||
using value_type = std::variant<convo::one_to_one, convo::community, convo::legacy_group>;
|
||||
using reference = value_type&;
|
||||
using pointer = value_type*;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
|
||||
struct iterator {
|
||||
protected:
|
||||
std::shared_ptr<convo::any> _val;
|
||||
std::optional<dict::const_iterator> _it_11, _end_11, _it_lgroup, _end_lgroup;
|
||||
std::optional<comm_iterator_helper> _it_comm;
|
||||
void _load_val();
|
||||
iterator() = default; // Constructs an end tombstone
|
||||
explicit iterator(
|
||||
const DictFieldRoot& data,
|
||||
bool oneto1 = true,
|
||||
bool communities = true,
|
||||
bool legacy_groups = true);
|
||||
friend class ConvoInfoVolatile;
|
||||
|
||||
public:
|
||||
bool operator==(const iterator& other) const;
|
||||
bool operator!=(const iterator& other) const { return !(*this == other); }
|
||||
bool done() const; // Equivalent to comparing against the end iterator
|
||||
convo::any& operator*() const { return *_val; }
|
||||
convo::any* operator->() const { return _val.get(); }
|
||||
iterator& operator++();
|
||||
iterator operator++(int) {
|
||||
auto copy{*this};
|
||||
++*this;
|
||||
return copy;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename ConvoType>
|
||||
struct subtype_iterator : iterator {
|
||||
protected:
|
||||
subtype_iterator(const DictFieldRoot& data) :
|
||||
iterator(
|
||||
data,
|
||||
std::is_same_v<convo::one_to_one, ConvoType>,
|
||||
std::is_same_v<convo::community, ConvoType>,
|
||||
std::is_same_v<convo::legacy_group, ConvoType>) {}
|
||||
friend class ConvoInfoVolatile;
|
||||
|
||||
public:
|
||||
ConvoType& operator*() const { return std::get<ConvoType>(*_val); }
|
||||
ConvoType* operator->() const { return &std::get<ConvoType>(*_val); }
|
||||
subtype_iterator& operator++() {
|
||||
iterator::operator++();
|
||||
return *this;
|
||||
}
|
||||
subtype_iterator operator++(int) {
|
||||
auto copy{*this};
|
||||
++*this;
|
||||
return copy;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
} // namespace session::config
|
|
@ -1,36 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/// Wrapper around session::config::encrypt. message and key_base are binary: message has the
|
||||
/// length provided, key_base must be exactly 32 bytes. domain is a c string. Returns a newly
|
||||
/// allocated buffer containing the encrypted data, and sets the data's length into
|
||||
/// `ciphertext_size`. It is the caller's responsibility to `free()` the returned buffer!
|
||||
///
|
||||
/// Returns nullptr on error.
|
||||
unsigned char* config_encrypt(
|
||||
const unsigned char* message,
|
||||
size_t mlen,
|
||||
const unsigned char* key_base,
|
||||
const char* domain,
|
||||
size_t* ciphertext_size);
|
||||
|
||||
/// Works just like config_encrypt, but in reverse.
|
||||
unsigned char* config_decrypt(
|
||||
const unsigned char* ciphertext,
|
||||
size_t clen,
|
||||
const unsigned char* key_base,
|
||||
const char* domain,
|
||||
size_t* plaintext_size);
|
||||
|
||||
/// Returns the amount of padding needed for a plaintext of size s with encryption overhead
|
||||
/// `overhead`.
|
||||
size_t config_padded_size(size_t s, size_t overhead);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
|
@ -1,69 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#include "../types.hpp"
|
||||
|
||||
namespace session::config {
|
||||
|
||||
/// Encrypts a config message using XChaCha20-Poly1305, using a blake2b keyed hash of the message
|
||||
/// for the nonce (rather than pure random) so that different clients will encrypt the same data to
|
||||
/// the same encrypted value (thus allowing for server-side deduplication of identical messages).
|
||||
///
|
||||
/// `key_base` must be 32 bytes. This value is a fixed key that all clients that might receive this
|
||||
/// message can calculate independently (for instance a value derived from a secret key, or a shared
|
||||
/// random key). This key will be hashed with the message size and domain suffix (see below) to
|
||||
/// determine the actual encryption key.
|
||||
///
|
||||
/// `domain` is a short string (1-24 chars) used for the keyed hash. Typically this is the type of
|
||||
/// config, e.g. "closed-group" or "contacts". The full key will be
|
||||
/// "session-config-encrypted-message-[domain]". This value is also used for the encrypted key (see
|
||||
/// above).
|
||||
///
|
||||
/// The returned result will consist of encrypted data with authentication tag and appended nonce,
|
||||
/// suitable for being passed to decrypt() to authenticate and decrypt.
|
||||
///
|
||||
/// Throw std::invalid_argument on bad input (i.e. from invalid key_base or domain).
|
||||
ustring encrypt(ustring_view message, ustring_view key_base, std::string_view domain);
|
||||
|
||||
/// Same as above, but modifies `message` in place. `message` gets encrypted plus has the extra
|
||||
/// data and nonce appended.
|
||||
void encrypt_inplace(ustring& message, ustring_view key_base, std::string_view domain);
|
||||
|
||||
/// Constant amount of extra bytes required to be appended when encrypting.
|
||||
constexpr size_t ENCRYPT_DATA_OVERHEAD = 40; // ABYTES + NPUBBYTES
|
||||
|
||||
/// Thrown if decrypt() fails.
|
||||
struct decrypt_error : std::runtime_error {
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
/// Takes a value produced by `encrypt()` and decrypts it. `key_base` and `domain` must be the same
|
||||
/// given to encrypt or else decryption fails. Upon decryption failure a `decrypt_error` exception
|
||||
/// is thrown.
|
||||
ustring decrypt(ustring_view ciphertext, ustring_view key_base, std::string_view domain);
|
||||
|
||||
/// Same as above, but does in in-place. The string gets shortend to the plaintext after this call.
|
||||
void decrypt_inplace(ustring& ciphertext, ustring_view key_base, std::string_view domain);
|
||||
|
||||
/// Returns the target size of the message with padding, assuming an additional `overhead` bytes of
|
||||
/// overhead (e.g. from encrypt() overhead) will be appended. Will always return a value >= s +
|
||||
/// overhead.
|
||||
///
|
||||
/// Padding increments we use: 256 byte increments up to 5120; 1024 byte increments up to 20480,
|
||||
/// 2048 increments up to 40960, then 5120 from there up.
|
||||
inline constexpr size_t padded_size(size_t s, size_t overhead = ENCRYPT_DATA_OVERHEAD) {
|
||||
size_t s2 = s + overhead;
|
||||
size_t chunk = s2 < 5120 ? 256 : s2 < 20480 ? 1024 : s2 < 40960 ? 2048 : 5120;
|
||||
return (s2 + chunk - 1) / chunk * chunk - overhead;
|
||||
}
|
||||
|
||||
/// Inserts null byte padding to the beginning of a message to make the final message size granular.
|
||||
/// See the above function for the sizes.
|
||||
///
|
||||
/// \param data - the data; this is modified in place.
|
||||
/// \param overhead - encryption overhead to account for to reach the desired padded size. The
|
||||
/// default, if omitted, is the space used by the `encrypt()` function defined above.
|
||||
void pad_message(ustring& data, size_t overhead = ENCRYPT_DATA_OVERHEAD);
|
||||
|
||||
} // namespace session::config
|
|
@ -1,23 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
enum config_error {
|
||||
/// Value returned for no error
|
||||
SESSION_ERR_NONE = 0,
|
||||
/// Error indicating that initialization failed because the dumped data being loaded is invalid.
|
||||
SESSION_ERR_INVALID_DUMP = 1,
|
||||
/// Error indicated a bad value, e.g. if trying to set something invalid in a config field.
|
||||
SESSION_ERR_BAD_VALUE = 2,
|
||||
};
|
||||
|
||||
// Returns a generic string for a given integer error code as returned by some functions. Depending
|
||||
// on the call, a more details error string may be available in the config_object's `last_error`
|
||||
// field.
|
||||
const char* config_errstr(int err);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
|
@ -1,7 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
typedef enum CONVO_EXPIRATION_MODE {
|
||||
CONVO_EXPIRATION_NONE = 0,
|
||||
CONVO_EXPIRATION_AFTER_SEND = 1,
|
||||
CONVO_EXPIRATION_AFTER_READ = 2,
|
||||
} CONVO_EXPIRATION_MODE;
|
|
@ -1,8 +0,0 @@
|
|||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace session::config {
|
||||
|
||||
enum class expiration_mode : int8_t { none = 0, after_send = 1, after_read = 2 };
|
||||
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace session::config {
|
||||
|
||||
enum class Namespace : std::int16_t {
|
||||
UserProfile = 2,
|
||||
Contacts = 3,
|
||||
ConvoInfoVolatile = 4,
|
||||
UserGroups = 5,
|
||||
};
|
||||
|
||||
} // namespace session::config
|
|
@ -1,8 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
typedef enum CONVO_NOTIFY_MODE {
|
||||
CONVO_NOTIFY_DEFAULT = 0,
|
||||
CONVO_NOTIFY_ALL = 1,
|
||||
CONVO_NOTIFY_DISABLED = 2,
|
||||
CONVO_NOTIFY_MENTIONS_ONLY = 3,
|
||||
} CONVO_NOTIFY_MODE;
|
|
@ -1,12 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
namespace session::config {
|
||||
|
||||
enum class notify_mode {
|
||||
defaulted = 0,
|
||||
all = 1,
|
||||
disabled = 2,
|
||||
mentions_only = 3, // Only for groups; for DMs this becomes `all`
|
||||
};
|
||||
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
// Maximum length of the profile pic URL (not including the null terminator)
|
||||
extern const size_t PROFILE_PIC_MAX_URL_LENGTH;
|
||||
|
||||
typedef struct user_profile_pic {
|
||||
// Null-terminated C string containing the uploaded URL of the pic. Will be length 0 if there
|
||||
// is no profile pic.
|
||||
char url[224];
|
||||
// The profile pic decryption key, in bytes. This is a byte buffer of length 32, *not* a
|
||||
// null-terminated C string. This is only valid when there is a url (i.e. url has strlen > 0).
|
||||
unsigned char key[32];
|
||||
} user_profile_pic;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
|
@ -1,57 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#include "session/types.hpp"
|
||||
|
||||
namespace session::config {
|
||||
|
||||
// Profile pic info.
|
||||
struct profile_pic {
|
||||
static constexpr size_t MAX_URL_LENGTH = 223;
|
||||
|
||||
std::string url;
|
||||
ustring key;
|
||||
|
||||
static void check_key(ustring_view key) {
|
||||
if (!(key.empty() || key.size() == 32))
|
||||
throw std::invalid_argument{"Invalid profile pic key: 32 bytes required"};
|
||||
}
|
||||
|
||||
// Default constructor, makes an empty profile pic
|
||||
profile_pic() = default;
|
||||
|
||||
// Constructs from a URL and key. Key must be empty or 32 bytes.
|
||||
profile_pic(std::string_view url, ustring_view key) : url{url}, key{key} {
|
||||
check_key(this->key);
|
||||
}
|
||||
|
||||
// Constructs from a string/ustring pair moved into the constructor
|
||||
profile_pic(std::string&& url, ustring&& key) : url{std::move(url)}, key{std::move(key)} {
|
||||
check_key(this->key);
|
||||
}
|
||||
|
||||
// Returns true if either url or key are empty (or invalid)
|
||||
bool empty() const { return url.empty() || key.size() != 32; }
|
||||
|
||||
// Clears the current url/key, if set. This is just a shortcut for calling `.clear()` on each
|
||||
// of them.
|
||||
void clear() {
|
||||
url.clear();
|
||||
key.clear();
|
||||
}
|
||||
|
||||
// The object in boolean context is true if url and key are both set, i.e. the opposite of
|
||||
// `empty()`.
|
||||
explicit operator bool() const { return !empty(); }
|
||||
|
||||
// Sets and validates the key. The key can be empty, or 32 bytes. This is almost the same as
|
||||
// just setting `.key` directly, except that it will throw if the provided key is invalid (i.e.
|
||||
// neither empty nor 32 bytes).
|
||||
void set_key(ustring new_key) {
|
||||
check_key(new_key);
|
||||
key = std::move(new_key);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace session::config
|
|
@ -1,271 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include "base.h"
|
||||
#include "notify.h"
|
||||
#include "util.h"
|
||||
|
||||
// Maximum length of a group name, in bytes
|
||||
extern const size_t GROUP_NAME_MAX_LENGTH;
|
||||
|
||||
/// Struct holding legacy group info; this struct owns allocated memory and *must* be freed via
|
||||
/// either `ugroups_legacy_group_free()` or `user_groups_set_free_legacy_group()` when finished with
|
||||
/// it.
|
||||
typedef struct ugroups_legacy_group_info {
|
||||
char session_id[67]; // in hex; 66 hex chars + null terminator.
|
||||
|
||||
char name[101]; // Null-terminated C string (human-readable). Max length is 100 (plus 1 for
|
||||
// null). Will always be set (even if an empty string).
|
||||
|
||||
bool have_enc_keys; // Will be true if we have an encryption keypair, false if not.
|
||||
unsigned char enc_pubkey[32]; // If `have_enc_keys`, this is the 32-byte pubkey (no NULL
|
||||
// terminator).
|
||||
unsigned char enc_seckey[32]; // If `have_enc_keys`, this is the 32-byte secret key (no NULL
|
||||
// terminator).
|
||||
|
||||
int64_t disappearing_timer; // Minutes. 0 == disabled.
|
||||
int priority; // pinned message priority; 0 = unpinned, negative = hidden, positive = pinned
|
||||
// (with higher meaning pinned higher).
|
||||
int64_t joined_at; // unix timestamp when joined (or re-joined)
|
||||
CONVO_NOTIFY_MODE notifications; // When the user wants notifications
|
||||
int64_t mute_until; // Mute notifications until this timestamp (overrides `notifications`
|
||||
// setting until the timestamp)
|
||||
|
||||
// For members use the ugroups_legacy_group_members and associated calls.
|
||||
|
||||
void* _internal; // Internal storage, do not touch.
|
||||
} ugroups_legacy_group_info;
|
||||
|
||||
typedef struct ugroups_community_info {
|
||||
char base_url[268]; // null-terminated (max length 267), normalized (i.e. always lower-case,
|
||||
// only has port if non-default, has trailing / removed)
|
||||
char room[65]; // null-terminated (max length 64); this is case-preserving (i.e. can be
|
||||
// "SomeRoom" instead of "someroom". Note this is different from volatile
|
||||
// info (that one is always forced lower-cased).
|
||||
unsigned char pubkey[32]; // 32 bytes (not terminated, can contain nulls)
|
||||
|
||||
int priority; // pinned message priority; 0 = unpinned, negative = hidden, positive = pinned
|
||||
// (with higher meaning pinned higher).
|
||||
int64_t joined_at; // unix timestamp when joined (or re-joined)
|
||||
CONVO_NOTIFY_MODE notifications; // When the user wants notifications
|
||||
int64_t mute_until; // Mute notifications until this timestamp (overrides `notifications`
|
||||
// setting until the timestamp)
|
||||
} ugroups_community_info;
|
||||
|
||||
int user_groups_init(
|
||||
config_object** conf,
|
||||
const unsigned char* ed25519_secretkey,
|
||||
const unsigned char* dump,
|
||||
size_t dumplen,
|
||||
char* error) __attribute__((warn_unused_result));
|
||||
|
||||
/// Gets community conversation info into `comm`, if the community info was found. `base_url` and
|
||||
/// `room` are null-terminated c strings; pubkey is 32 bytes. base_url will be
|
||||
/// normalized/lower-cased; room is case-insensitive for the lookup: note that this may well return
|
||||
/// a community info with a different room capitalization than the one provided to the call.
|
||||
///
|
||||
/// Returns true if the community was found and `comm` populated; false otherwise. A false return
|
||||
/// can either be because it didn't exist (`conf->last_error` will be NULL) or because of some error
|
||||
/// (`last_error` will be set to an error string).
|
||||
bool user_groups_get_community(
|
||||
config_object* conf,
|
||||
ugroups_community_info* comm,
|
||||
const char* base_url,
|
||||
const char* room) __attribute__((warn_unused_result));
|
||||
|
||||
/// Like the above, but if the community was not found, this constructs one that can be inserted.
|
||||
/// `base_url` will be normalized in the returned object. `room` is a case-insensitive lookup key
|
||||
/// for the room token. Note that it has subtle handling w.r.t its case: if an existing room is
|
||||
/// found, you get back a record with the found case (which could differ in case from what you
|
||||
/// provided). If you want to override to what you provided regardless of what is there you should
|
||||
/// immediately set the name of the returned object to the case you prefer. If a *new* record is
|
||||
/// constructed, however, it will match the room token case as given here.
|
||||
///
|
||||
/// Note that this is all different from convo_info_volatile, which always forces the room token to
|
||||
/// lower-case (because it does not preserve the case).
|
||||
///
|
||||
/// Returns false (and sets `conf->last_error`) on error.
|
||||
bool user_groups_get_or_construct_community(
|
||||
config_object* conf,
|
||||
ugroups_community_info* comm,
|
||||
const char* base_url,
|
||||
const char* room,
|
||||
unsigned const char* pubkey) __attribute__((warn_unused_result));
|
||||
|
||||
/// Returns a ugroups_legacy_group_info pointer containing the conversation info for a given legacy
|
||||
/// group ID (specified as a null-terminated hex string), if the conversation exists. If the
|
||||
/// conversation does not exist, returns NULL. Sets conf->last_error on error.
|
||||
///
|
||||
/// The returned pointer *must* be freed either by calling `ugroups_legacy_group_free()` when done
|
||||
/// with it, or by passing it to `user_groups_set_free_legacy_group()`.
|
||||
ugroups_legacy_group_info* user_groups_get_legacy_group(config_object* conf, const char* id)
|
||||
__attribute__((warn_unused_result));
|
||||
|
||||
/// Same as the above except that when the conversation does not exist, this sets all the group
|
||||
/// fields to defaults and loads it with the given id.
|
||||
///
|
||||
/// Returns a ugroups_legacy_group_info as long as it is given a valid legacy group id (i.e. same
|
||||
/// format as a session id); it will return NULL only if the given id is invalid (and so the caller
|
||||
/// needs to either pre-validate the id, or post-validate the return value).
|
||||
///
|
||||
/// The returned pointer *must* be freed either by calling `ugroups_legacy_group_free()` when done
|
||||
/// with it, or by passing it to `user_groups_set_free_legacy_group()`.
|
||||
///
|
||||
/// This is the method that should usually be used to create or update a conversation, followed by
|
||||
/// setting fields in the group, and then giving it to user_groups_set().
|
||||
///
|
||||
/// On error, this returns NULL and sets `conf->last_error`.
|
||||
ugroups_legacy_group_info* user_groups_get_or_construct_legacy_group(
|
||||
config_object* conf, const char* id) __attribute__((warn_unused_result));
|
||||
|
||||
/// Properly frees memory associated with a ugroups_legacy_group_info pointer (as returned by
|
||||
/// get_legacy_group/get_or_construct_legacy_group).
|
||||
void ugroups_legacy_group_free(ugroups_legacy_group_info* group);
|
||||
|
||||
/// Adds or updates a community conversation from the given group info
|
||||
void user_groups_set_community(config_object* conf, const ugroups_community_info* group);
|
||||
|
||||
/// Adds or updates a legacy group conversation from the into. This version of the method should
|
||||
/// only be used when you explicitly want the `group` to remain valid; if the set is the last thing
|
||||
/// you need to do with it (which is common) it is more efficient to call the freeing version,
|
||||
/// below.
|
||||
void user_groups_set_legacy_group(config_object* conf, const ugroups_legacy_group_info* group);
|
||||
|
||||
/// Same as above, except that this also frees the pointer for you, which is commonly what is wanted
|
||||
/// when updating fields. This is equivalent to, but more efficient than, setting and then freeing.
|
||||
void user_groups_set_free_legacy_group(config_object* conf, ugroups_legacy_group_info* group);
|
||||
|
||||
/// Erases a conversation from the conversation list. Returns true if the conversation was found
|
||||
/// and removed, false if the conversation was not present. You must not call this during
|
||||
/// iteration; see details below.
|
||||
bool user_groups_erase_community(config_object* conf, const char* base_url, const char* room);
|
||||
bool user_groups_erase_legacy_group(config_object* conf, const char* group_id);
|
||||
|
||||
typedef struct ugroups_legacy_members_iterator ugroups_legacy_members_iterator;
|
||||
|
||||
/// Group member iteration; this lets you walk through the full group member list. Example usage:
|
||||
///
|
||||
/// const char* session_id;
|
||||
/// bool admin;
|
||||
/// ugroups_legacy_members_iterator* it = ugroups_legacy_members_begin(legacy_info);
|
||||
/// while (ugroups_legacy_members_next(it, &session_id, &admin)) {
|
||||
/// if (admin)
|
||||
/// printf("ADMIN: %s", session_id);
|
||||
/// }
|
||||
/// ugroups_legacy_members_free(it);
|
||||
///
|
||||
ugroups_legacy_members_iterator* ugroups_legacy_members_begin(ugroups_legacy_group_info* group);
|
||||
bool ugroups_legacy_members_next(
|
||||
ugroups_legacy_members_iterator* it, const char** session_id, bool* admin);
|
||||
void ugroups_legacy_members_free(ugroups_legacy_members_iterator* it);
|
||||
|
||||
/// This erases the group member at the current iteration location during a member iteration,
|
||||
/// allowing iteration to continue.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// while (ugroups_legacy_members_next(it, &sid, &admin)) {
|
||||
/// if (should_remove(sid))
|
||||
/// ugroups_legacy_members_erase(it);
|
||||
/// }
|
||||
void ugroups_legacy_members_erase(ugroups_legacy_members_iterator* it);
|
||||
|
||||
/// Adds a member (by session id and admin status) to this group. Returns true if the member was
|
||||
/// inserted or had the admin status changed, false if the member already existed with the given
|
||||
/// status, or if the session_id is not valid.
|
||||
bool ugroups_legacy_member_add(
|
||||
ugroups_legacy_group_info* group, const char* session_id, bool admin);
|
||||
|
||||
/// Removes a member (including admins) from the group given the member's session id. This is not
|
||||
/// safe to use on the current member during member iteration; for that see the above method
|
||||
/// instead. Returns true if the session id was found and removed, false if not found.
|
||||
bool ugroups_legacy_member_remove(ugroups_legacy_group_info* group, const char* session_id);
|
||||
|
||||
/// Accesses the number of members in the group. The overall number is returned (both admins and
|
||||
/// non-admins); if the given variables are not NULL, they will be populated with the individual
|
||||
/// counts of members/admins.
|
||||
size_t ugroups_legacy_members_count(
|
||||
const ugroups_legacy_group_info* group, size_t* members, size_t* admins);
|
||||
|
||||
/// Returns the number of conversations.
|
||||
size_t user_groups_size(const config_object* conf);
|
||||
/// Returns the number of conversations of the specific type.
|
||||
size_t user_groups_size_communities(const config_object* conf);
|
||||
size_t user_groups_size_legacy_groups(const config_object* conf);
|
||||
|
||||
/// Functions for iterating through the entire conversation list. Intended use is:
|
||||
///
|
||||
/// ugroups_community_info c2;
|
||||
/// ugroups_legacy_group_info c3;
|
||||
/// user_groups_iterator *it = user_groups_iterator_new(my_groups);
|
||||
/// for (; !user_groups_iterator_done(it); user_groups_iterator_advance(it)) {
|
||||
/// if (user_groups_it_is_community(it, &c2)) {
|
||||
/// // use c2.whatever
|
||||
/// } else if (user_groups_it_is_legacy_group(it, &c3)) {
|
||||
/// // use c3.whatever
|
||||
/// }
|
||||
/// }
|
||||
/// user_groups_iterator_free(it);
|
||||
///
|
||||
/// It is permitted to modify records (e.g. with a call to one of the `user_groups_set_*`
|
||||
/// functions) and add records while iterating.
|
||||
///
|
||||
/// If you need to remove while iterating then usage is slightly different: you must advance the
|
||||
/// iteration by calling either user_groups_iterator_advance if not deleting, or
|
||||
/// user_groups_iterator_erase to erase and advance. Usage looks like this:
|
||||
///
|
||||
/// ugroups_community_info comm;
|
||||
/// ugroups_iterator *it = ugroups_iterator_new(my_groups);
|
||||
/// while (!user_groups_iterator_done(it)) {
|
||||
/// if (user_groups_it_is_community(it, &comm)) {
|
||||
/// bool should_delete = /* ... */;
|
||||
/// if (should_delete)
|
||||
/// user_groups_iterator_erase(it);
|
||||
/// else
|
||||
/// user_groups_iterator_advance(it);
|
||||
/// } else {
|
||||
/// user_groups_iterator_advance(it);
|
||||
/// }
|
||||
/// }
|
||||
/// user_groups_iterator_free(it);
|
||||
///
|
||||
|
||||
typedef struct user_groups_iterator user_groups_iterator;
|
||||
|
||||
// Starts a new iterator that iterates over all conversations.
|
||||
user_groups_iterator* user_groups_iterator_new(const config_object* conf);
|
||||
|
||||
// The same as `user_groups_iterator_new` except that this iterates *only* over one type of
|
||||
// conversation. You still need to use `user_groups_it_is_community` (or the alternatives)
|
||||
// to load the data in each pass of the loop. (You can, however, safely ignore the bool return
|
||||
// value of the `it_is_whatever` function: it will always be true for the particular type being
|
||||
// iterated over).
|
||||
user_groups_iterator* user_groups_iterator_new_communities(const config_object* conf);
|
||||
user_groups_iterator* user_groups_iterator_new_legacy_groups(const config_object* conf);
|
||||
|
||||
// Frees an iterator once no longer needed.
|
||||
void user_groups_iterator_free(user_groups_iterator* it);
|
||||
|
||||
// Returns true if iteration has reached the end.
|
||||
bool user_groups_iterator_done(user_groups_iterator* it);
|
||||
|
||||
// Advances the iterator.
|
||||
void user_groups_iterator_advance(user_groups_iterator* it);
|
||||
|
||||
// If the current iterator record is a community conversation this sets the details into `c` and
|
||||
// returns true. Otherwise it returns false.
|
||||
bool user_groups_it_is_community(user_groups_iterator* it, ugroups_community_info* c);
|
||||
|
||||
// If the current iterator record is a legacy group conversation this sets the details into
|
||||
// `c` and returns true. Otherwise it returns false.
|
||||
bool user_groups_it_is_legacy_group(user_groups_iterator* it, ugroups_legacy_group_info* c);
|
||||
|
||||
// Erases the current group while advancing the iterator to the next group in the iteration.
|
||||
void user_groups_iterator_erase(config_object* conf, user_groups_iterator* it);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
|
@ -1,366 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <session/config.hpp>
|
||||
|
||||
#include "base.hpp"
|
||||
#include "community.hpp"
|
||||
#include "namespaces.hpp"
|
||||
#include "notify.hpp"
|
||||
|
||||
extern "C" {
|
||||
struct ugroups_legacy_group_info;
|
||||
struct ugroups_community_info;
|
||||
}
|
||||
|
||||
namespace session::config {
|
||||
|
||||
/// keys used in this config, either currently or in the past (so that we don't reuse):
|
||||
///
|
||||
/// C - dict of legacy groups; within this dict each key is the group pubkey (binary, 33 bytes) and
|
||||
/// value is a dict containing keys:
|
||||
///
|
||||
/// n - name (string). Always set, even if empty.
|
||||
/// k - encryption public key (32 bytes). Optional.
|
||||
/// K - encryption secret key (32 bytes). Optional.
|
||||
/// m - set of member session ids (each 33 bytes).
|
||||
/// a - set of admin session ids (each 33 bytes).
|
||||
/// E - disappearing messages duration, in seconds, > 0. Omitted if disappearing messages is
|
||||
/// disabled. (Note that legacy groups only support expire after-read)
|
||||
/// @ - notification setting (int). Omitted = use default setting; 1 = all, 2 = disabled, 3 =
|
||||
/// mentions-only.
|
||||
/// ! - mute timestamp: if set then don't show notifications for this contact's messages until
|
||||
/// this unix timestamp (i.e. overriding the current notification setting until the given
|
||||
/// time).
|
||||
/// + - the conversation priority, for pinned/hidden messages. Integer. Omitted means not
|
||||
/// pinned; -1 means hidden, and a positive value is a pinned message for which higher
|
||||
/// priority values means the conversation is meant to appear earlier in the pinned
|
||||
/// conversation list.
|
||||
/// j - joined at unix timestamp. Omitted if 0.
|
||||
///
|
||||
/// o - dict of communities (AKA open groups); within this dict (which deliberately has the same
|
||||
/// layout as convo_info_volatile) each key is the SOGS base URL (in canonical form), and value
|
||||
/// is a dict of:
|
||||
///
|
||||
/// # - server pubkey
|
||||
/// R - dict of rooms on the server. Each key is the *lower-case* room name; each value is:
|
||||
/// n - the room name as is commonly used, i.e. with possible capitalization (if
|
||||
/// appropriate). For instance, a room name SudokuSolvers would be "sudokusolvers" in
|
||||
/// the outer key, with the capitalization variation in use ("SudokuSolvers") in this
|
||||
/// key. This key is *always* present (to keep the room dict non-empty).
|
||||
/// @ - notification setting (see above).
|
||||
/// ! - mute timestamp (see above).
|
||||
/// + - the conversation priority, for pinned messages. Omitted means not pinned; -1 means
|
||||
/// hidden; otherwise an integer value >0, where a higher priority means the
|
||||
/// conversation is meant to appear earlier in the pinned conversation list.
|
||||
/// j - joined at unix timestamp. Omitted if 0.
|
||||
///
|
||||
/// c - reserved for future storage of new-style group info.
|
||||
|
||||
/// Common base type with fields shared by all the groups
|
||||
struct base_group_info {
|
||||
int priority = 0; // The priority; 0 means unpinned, -1 means hidden, positive means
|
||||
// pinned higher (i.e. higher priority conversations come first).
|
||||
int64_t joined_at = 0; // unix timestamp (seconds) when the group was joined (or re-joined)
|
||||
notify_mode notifications = notify_mode::defaulted; // When the user wants notifications
|
||||
int64_t mute_until = 0; // unix timestamp (seconds) until which notifications are disabled
|
||||
|
||||
protected:
|
||||
void load(const dict& info_dict);
|
||||
};
|
||||
|
||||
/// Struct containing legacy group info (aka "closed groups").
|
||||
struct legacy_group_info : base_group_info {
|
||||
static constexpr size_t NAME_MAX_LENGTH = 100; // in bytes; name will be truncated if exceeded
|
||||
|
||||
std::string session_id; // The legacy group "session id" (33 bytes).
|
||||
std::string name; // human-readable; this should normally always be set, but in theory could be
|
||||
// set to an empty string.
|
||||
ustring enc_pubkey; // bytes (32 or empty)
|
||||
ustring enc_seckey; // bytes (32 or empty)
|
||||
std::chrono::seconds disappearing_timer{0}; // 0 == disabled.
|
||||
|
||||
/// Constructs a new legacy group info from an id (which must look like a session_id). Throws
|
||||
/// if id is invalid.
|
||||
explicit legacy_group_info(std::string sid);
|
||||
|
||||
// Accesses the session ids (in hex) of members of this group. The key is the hex session_id;
|
||||
// the value indicates whether the member is an admin (true) or not (false).
|
||||
const std::map<std::string, bool>& members() const { return members_; }
|
||||
|
||||
// Returns a pair of the number of admins, and regular members of this group. (If all you want
|
||||
// is the overall number just use `.members().size()` instead).
|
||||
std::pair<size_t, size_t> counts() const;
|
||||
|
||||
// Adds a member (by session id and admin status) to this group. Returns true if the member was
|
||||
// inserted or changed admin status, false if the member already existed. Throws
|
||||
// std::invalid_argument if the given session id is invalid.
|
||||
bool insert(std::string session_id, bool admin);
|
||||
|
||||
// Removes a member (by session id) from this group. Returns true if the member was
|
||||
// removed, false if the member was not present.
|
||||
bool erase(const std::string& session_id);
|
||||
|
||||
// Internal ctor/method for C API implementations:
|
||||
legacy_group_info(const struct ugroups_legacy_group_info& c); // From c struct
|
||||
legacy_group_info(struct ugroups_legacy_group_info&& c); // From c struct
|
||||
void into(struct ugroups_legacy_group_info& c) const&; // Copy into c struct
|
||||
void into(struct ugroups_legacy_group_info& c) &&; // Move into c struct
|
||||
|
||||
private:
|
||||
// session_id => (is admin)
|
||||
std::map<std::string, bool> members_;
|
||||
|
||||
friend class UserGroups;
|
||||
|
||||
// Private implementations of the to/from C struct methods
|
||||
struct impl_t {};
|
||||
static constexpr inline impl_t impl{};
|
||||
legacy_group_info(const struct ugroups_legacy_group_info& c, impl_t);
|
||||
void into(struct ugroups_legacy_group_info& c, impl_t) const;
|
||||
|
||||
void load(const dict& info_dict);
|
||||
};
|
||||
|
||||
/// Community (aka open group) info
|
||||
struct community_info : base_group_info, community {
|
||||
// Note that *changing* url/room/pubkey and then doing a set inserts a new room under the given
|
||||
// url/room/pubkey, it does *not* update an existing room.
|
||||
|
||||
// See community_base (comm_base.hpp) for common constructors
|
||||
using community::community;
|
||||
|
||||
// Internal ctor/method for C API implementations:
|
||||
community_info(const struct ugroups_community_info& c); // From c struct
|
||||
void into(ugroups_community_info& c) const; // Into c struct
|
||||
|
||||
private:
|
||||
void load(const dict& info_dict);
|
||||
|
||||
friend class UserGroups;
|
||||
friend class comm_iterator_helper;
|
||||
};
|
||||
|
||||
using any_group_info = std::variant<community_info, legacy_group_info>;
|
||||
|
||||
class UserGroups : public ConfigBase {
|
||||
|
||||
public:
|
||||
// No default constructor
|
||||
UserGroups() = delete;
|
||||
|
||||
/// Constructs a user group list from existing data (stored from `dump()`) and the user's
|
||||
/// secret key for generating the data encryption key. To construct a blank list (i.e. with no
|
||||
/// pre-existing dumped data to load) pass `std::nullopt` as the second argument.
|
||||
///
|
||||
/// \param ed25519_secretkey - contains the libsodium secret key used to encrypt/decrypt the
|
||||
/// data when pushing/pulling from the swarm. This can either be the full 64-byte value (which
|
||||
/// is technically the 32-byte seed followed by the 32-byte pubkey), or just the 32-byte seed of
|
||||
/// the secret key.
|
||||
///
|
||||
/// \param dumped - either `std::nullopt` to construct a new, empty object; or binary state data
|
||||
/// that was previously dumped from an instance of this class by calling `dump()`.
|
||||
UserGroups(ustring_view ed25519_secretkey, std::optional<ustring_view> dumped);
|
||||
|
||||
Namespace storage_namespace() const override { return Namespace::UserGroups; }
|
||||
|
||||
const char* encryption_domain() const override { return "UserGroups"; }
|
||||
|
||||
/// Looks up and returns a community (aka open group) conversation. Takes the base URL and room
|
||||
/// token (case insensitive). Retuns nullopt if the open group was not found, otherwise a
|
||||
/// filled out `community_info`. Note that the `room` argument here is case-insensitive, but
|
||||
/// the returned value will be the room as stored in the object (i.e. it may have a different
|
||||
/// case from the requested `room` value).
|
||||
std::optional<community_info> get_community(
|
||||
std::string_view base_url, std::string_view room) const;
|
||||
|
||||
/// Looks up a community from a full URL. It is permitted for the URL to omit the pubkey (it
|
||||
/// is not used or needed by this call).
|
||||
std::optional<community_info> get_community(std::string_view partial_url) const;
|
||||
|
||||
/// Looks up and returns a legacy group by group ID (hex, looks like a Session ID). Returns
|
||||
/// nullopt if the group was not found, otherwise returns a filled out `legacy_group_info`.
|
||||
std::optional<legacy_group_info> get_legacy_group(std::string_view pubkey_hex) const;
|
||||
|
||||
/// Same as `get_community`, except if the community isn't found a new blank one is created for
|
||||
/// you, prefilled with the url/room/pubkey.
|
||||
///
|
||||
/// Note that `room` and `pubkey` have special handling:
|
||||
/// - `room` is case-insensitive for the lookup: if a matching room is found then the returned
|
||||
/// value reflects the room case of the existing record, which is not necessarily the same as
|
||||
/// the `room` argument given here (to force a case change, set it within the returned
|
||||
/// object).
|
||||
/// - `pubkey` is not used to find an existing community, but if the community found has a
|
||||
/// *different* pubkey from the one given then the returned record has its pubkey updated in
|
||||
/// the return instance (note that this changed value is not committed to storage, however,
|
||||
/// until the instance is passed to `set()`). For the string_view version the pubkey is
|
||||
/// accepted as hex, base32z, or base64.
|
||||
community_info get_or_construct_community(
|
||||
std::string_view base_url,
|
||||
std::string_view room,
|
||||
std::string_view pubkey_encoded) const;
|
||||
community_info get_or_construct_community(
|
||||
std::string_view base_url, std::string_view room, ustring_view pubkey) const;
|
||||
/// Shortcut to pass the url through community::parse_full_url, then call the above.
|
||||
community_info get_or_construct_community(std::string_view full_url) const;
|
||||
|
||||
/// Gets or constructs a blank legacy_group_info for the given group id.
|
||||
legacy_group_info get_or_construct_legacy_group(std::string_view pubkey_hex) const;
|
||||
|
||||
/// Inserts or replaces existing group info. For example, to update the info for a community
|
||||
/// you would do:
|
||||
///
|
||||
/// auto info = conversations.get_or_construct_community(some_session_id);
|
||||
/// info.last_read = new_unix_timestamp;
|
||||
/// conversations.set(info);
|
||||
///
|
||||
void set(const community_info& info);
|
||||
void set(const legacy_group_info& info);
|
||||
/// Takes a variant of either group type to set:
|
||||
void set(const any_group_info& info);
|
||||
|
||||
protected:
|
||||
// Drills into the nested dicts to access open group details
|
||||
DictFieldProxy community_field(
|
||||
const community_info& og, ustring_view* get_pubkey = nullptr) const;
|
||||
|
||||
void set_base(const base_group_info& bg, DictFieldProxy& info) const;
|
||||
|
||||
public:
|
||||
/// Removes a community group. Returns true if found and removed, false if not present.
|
||||
/// Arguments are the same as `get_community`.
|
||||
bool erase_community(std::string_view base_url, std::string_view room);
|
||||
|
||||
/// Removes a legacy group conversation. Returns true if found and removed, false if not
|
||||
/// present.
|
||||
bool erase_legacy_group(std::string_view pubkey_hex);
|
||||
|
||||
/// Removes a conversation taking the community_info or legacy_group_info instance (rather than
|
||||
/// the pubkey/url) for convenience.
|
||||
bool erase(const community_info& g);
|
||||
bool erase(const legacy_group_info& c);
|
||||
bool erase(const any_group_info& info);
|
||||
|
||||
struct iterator;
|
||||
|
||||
/// This works like erase, but takes an iterator to the group to remove. The element is removed
|
||||
/// and the iterator to the next element after the removed one is returned. This is intended
|
||||
/// for use where elements are to be removed during iteration: see below for an example.
|
||||
iterator erase(iterator it);
|
||||
|
||||
/// Returns the number of groups (of any type).
|
||||
size_t size() const;
|
||||
|
||||
/// Returns the number of communities
|
||||
size_t size_communities() const;
|
||||
|
||||
/// Returns the number of legacy groups
|
||||
size_t size_legacy_groups() const;
|
||||
|
||||
/// Returns true if the group list is empty.
|
||||
bool empty() const { return size() == 0; }
|
||||
|
||||
/// Iterators for iterating through all groups. Typically you access this implicit via a
|
||||
/// for loop over the `UserGroups` object:
|
||||
///
|
||||
/// for (auto& group : usergroups) {
|
||||
/// if (auto* comm = std::get_if<community_info>(&group)) {
|
||||
/// // use comm->name, comm->priority, etc.
|
||||
/// } else if (auto* lg = std::get_if<legacy_group_info>(&convo)) {
|
||||
/// // use lg->session_id, lg->priority, etc.
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// This iterates through all groups in sorted order (sorted first by convo type, then by
|
||||
/// id within the type).
|
||||
///
|
||||
/// It is permitted to modify and add records while iterating (e.g. by modifying one of the
|
||||
/// `comm`/`lg` objects and then calling set()).
|
||||
///
|
||||
/// If you need to erase the current conversation during iteration then care is required: you
|
||||
/// need to advance the iterator via the iterator version of erase when erasing an element
|
||||
/// rather than incrementing it regularly. For example:
|
||||
///
|
||||
/// for (auto it = conversations.begin(); it != conversations.end(); ) {
|
||||
/// if (should_remove(*it))
|
||||
/// it = converations.erase(it);
|
||||
/// else
|
||||
/// ++it;
|
||||
/// }
|
||||
///
|
||||
/// Alternatively, you can use the first version with two loops: the first loop through all
|
||||
/// converations doesn't erase but just builds a vector of IDs to erase, then the second loops
|
||||
/// through that vector calling `erase_1to1()`/`erase_open()`/`erase_legacy_group()` for each
|
||||
/// one.
|
||||
///
|
||||
iterator begin() const { return iterator{data}; }
|
||||
iterator end() const { return iterator{}; }
|
||||
|
||||
template <typename GroupType>
|
||||
struct subtype_iterator;
|
||||
|
||||
/// Returns an iterator that iterates only through one type of conversations. (The regular
|
||||
/// `.end()` iterator is valid for testing the end of these iterations).
|
||||
subtype_iterator<community_info> begin_communities() const { return {data}; }
|
||||
subtype_iterator<legacy_group_info> begin_legacy_groups() const { return {data}; }
|
||||
|
||||
using iterator_category = std::input_iterator_tag;
|
||||
using value_type = std::variant<community_info, legacy_group_info>;
|
||||
using reference = value_type&;
|
||||
using pointer = value_type*;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
|
||||
struct iterator {
|
||||
protected:
|
||||
std::shared_ptr<any_group_info> _val;
|
||||
std::optional<comm_iterator_helper> _it_comm;
|
||||
std::optional<dict::const_iterator> _it_legacy, _end_legacy;
|
||||
void _load_val();
|
||||
iterator() = default; // Constructs an end tombstone
|
||||
explicit iterator(
|
||||
const DictFieldRoot& data, bool communities = true, bool legacy_closed = true);
|
||||
friend class UserGroups;
|
||||
|
||||
public:
|
||||
bool operator==(const iterator& other) const;
|
||||
bool operator!=(const iterator& other) const { return !(*this == other); }
|
||||
bool done() const; // Equivalent to comparing against the end iterator
|
||||
any_group_info& operator*() const { return *_val; }
|
||||
any_group_info* operator->() const { return _val.get(); }
|
||||
iterator& operator++();
|
||||
iterator operator++(int) {
|
||||
auto copy{*this};
|
||||
++*this;
|
||||
return copy;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename GroupType>
|
||||
struct subtype_iterator : iterator {
|
||||
protected:
|
||||
subtype_iterator(const DictFieldRoot& data) :
|
||||
iterator(
|
||||
data,
|
||||
std::is_same_v<community_info, GroupType>,
|
||||
std::is_same_v<legacy_group_info, GroupType>) {}
|
||||
friend class UserGroups;
|
||||
|
||||
public:
|
||||
GroupType& operator*() const { return std::get<GroupType>(*_val); }
|
||||
GroupType* operator->() const { return &std::get<GroupType>(*_val); }
|
||||
subtype_iterator& operator++() {
|
||||
iterator::operator++();
|
||||
return *this;
|
||||
}
|
||||
subtype_iterator operator++(int) {
|
||||
auto copy{*this};
|
||||
++*this;
|
||||
return copy;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
} // namespace session::config
|
|
@ -1,63 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include "base.h"
|
||||
#include "profile_pic.h"
|
||||
|
||||
/// Constructs a user profile config object and sets a pointer to it in `conf`.
|
||||
///
|
||||
/// \param ed25519_secretkey must be the 32-byte secret key seed value. (You can also pass the
|
||||
/// pointer to the beginning of the 64-byte value libsodium calls the "secret key" as the first 32
|
||||
/// bytes of that are the seed). This field cannot be null.
|
||||
///
|
||||
/// \param dump - if non-NULL this restores the state from the dumped byte string produced by a past
|
||||
/// instantiation's call to `dump()`. To construct a new, empty profile this should be NULL.
|
||||
///
|
||||
/// \param dumplen - the length of `dump` when restoring from a dump, or 0 when `dump` is NULL.
|
||||
///
|
||||
/// \param error - the pointer to a buffer in which we will write an error string if an error
|
||||
/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a
|
||||
/// buffer of at least 256 bytes.
|
||||
///
|
||||
/// Returns 0 on success; returns a non-zero error code and write the exception message as a
|
||||
/// C-string into `error` (if not NULL) on failure.
|
||||
///
|
||||
/// When done with the object the `config_object` must be destroyed by passing the pointer to
|
||||
/// config_free() (in `session/config/base.h`).
|
||||
int user_profile_init(
|
||||
config_object** conf,
|
||||
const unsigned char* ed25519_secretkey,
|
||||
const unsigned char* dump,
|
||||
size_t dumplen,
|
||||
char* error) __attribute__((warn_unused_result));
|
||||
|
||||
/// Returns a pointer to the currently-set name (null-terminated), or NULL if there is no name at
|
||||
/// all. Should be copied right away as the pointer may not remain valid beyond other API calls.
|
||||
const char* user_profile_get_name(const config_object* conf);
|
||||
|
||||
/// Sets the user profile name to the null-terminated C string. Returns 0 on success, non-zero on
|
||||
/// error (and sets the config_object's error string).
|
||||
int user_profile_set_name(config_object* conf, const char* name);
|
||||
|
||||
// Obtains the current profile pic. The pointers in the returned struct will be NULL if a profile
|
||||
// pic is not currently set, and otherwise should be copied right away (they will not be valid
|
||||
// beyond other API calls on this config object).
|
||||
user_profile_pic user_profile_get_pic(const config_object* conf);
|
||||
|
||||
// Sets a user profile
|
||||
int user_profile_set_pic(config_object* conf, user_profile_pic pic);
|
||||
|
||||
// Gets the current note-to-self priority level. Will be negative for hidden, 0 for unpinned, and >
|
||||
// 0 for pinned (with higher value = higher priority).
|
||||
int user_profile_get_nts_priority(const config_object* conf);
|
||||
|
||||
// Sets the current note-to-self priority level. Set to -1 for hidden; 0 for unpinned, and > 0 for
|
||||
// higher priority in the conversation list.
|
||||
void user_profile_set_nts_priority(config_object* conf, int priority);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
|
@ -1,67 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <session/config.hpp>
|
||||
|
||||
#include "base.hpp"
|
||||
#include "namespaces.hpp"
|
||||
#include "profile_pic.hpp"
|
||||
|
||||
namespace session::config {
|
||||
|
||||
/// keys used in this config, either currently or in the past (so that we don't reuse):
|
||||
///
|
||||
/// n - user profile name
|
||||
/// p - user profile url
|
||||
/// q - user profile decryption key (binary)
|
||||
/// + - the priority value for the "Note to Self" pseudo-conversation (higher = higher in the
|
||||
/// conversation list). Omitted when 0. -1 means hidden.
|
||||
|
||||
class UserProfile final : public ConfigBase {
|
||||
|
||||
public:
|
||||
// No default constructor
|
||||
UserProfile() = delete;
|
||||
|
||||
/// Constructs a user profile from existing data (stored from `dump()`) and the user's secret
|
||||
/// key for generating the data encryption key. To construct a blank profile (i.e. with no
|
||||
/// pre-existing dumped data to load) pass `std::nullopt` as the second argument.
|
||||
///
|
||||
/// \param ed25519_secretkey - contains the libsodium secret key used to encrypt/decrypt the
|
||||
/// data when pushing/pulling from the swarm. This can either be the full 64-byte value (which
|
||||
/// is technically the 32-byte seed followed by the 32-byte pubkey), or just the 32-byte seed of
|
||||
/// the secret key.
|
||||
///
|
||||
/// \param dumped - either `std::nullopt` to construct a new, empty object; or binary state data
|
||||
/// that was previously dumped from an instance of this class by calling `dump()`.
|
||||
UserProfile(ustring_view ed25519_secretkey, std::optional<ustring_view> dumped);
|
||||
|
||||
Namespace storage_namespace() const override { return Namespace::UserProfile; }
|
||||
|
||||
const char* encryption_domain() const override { return "UserProfile"; }
|
||||
|
||||
/// Returns the user profile name, or std::nullopt if there is no profile name set.
|
||||
std::optional<std::string_view> get_name() const;
|
||||
|
||||
/// Sets the user profile name; if given an empty string then the name is removed.
|
||||
void set_name(std::string_view new_name);
|
||||
|
||||
/// Gets the user's current profile pic URL and decryption key. The returned object will
|
||||
/// evaluate as false if the URL and/or key are not set.
|
||||
profile_pic get_profile_pic() const;
|
||||
|
||||
/// Sets the user's current profile pic to a new URL and decryption key. Clears both if either
|
||||
/// one is empty.
|
||||
void set_profile_pic(std::string_view url, ustring_view key);
|
||||
void set_profile_pic(profile_pic pic);
|
||||
|
||||
/// Gets the Note-to-self conversation priority. Negative means hidden; 0 means unpinned;
|
||||
/// higher means higher priority (i.e. hidden in the convo list).
|
||||
int get_nts_priority() const;
|
||||
|
||||
/// Sets the Note-to-self conversation priority. -1 for hidden, 0 for unpinned, higher for
|
||||
/// pinned higher.
|
||||
void set_nts_priority(int priority);
|
||||
};
|
||||
|
||||
} // namespace session::config
|
|
@ -1,16 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
/// Returns true if session_id has the right form (66 hex digits). This is a quick check, not a
|
||||
/// robust one: it does not check the leading byte prefix, nor the cryptographic properties of the
|
||||
/// pubkey for actual validity.
|
||||
bool session_id_is_valid(const char* session_id);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
|
@ -1,8 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#if defined(_WIN32) || defined(WIN32)
|
||||
#define LIBSESSION_EXPORT __declspec(dllexport)
|
||||
#else
|
||||
#define LIBSESSION_EXPORT __attribute__((visibility("default")))
|
||||
#endif
|
||||
#define LIBSESSION_C_API extern "C" LIBSESSION_EXPORT
|
|
@ -1,43 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace session {
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
/// An uploaded file is its URL + decryption key
|
||||
struct Uploaded {
|
||||
std::string url;
|
||||
std::string key;
|
||||
};
|
||||
|
||||
/// A conversation disappearing messages setting
|
||||
struct Disappearing {
|
||||
/// The possible modes of a disappearing messages setting.
|
||||
enum class Mode : int { None = 0, AfterSend = 1, AfterRead = 2 };
|
||||
|
||||
/// The mode itself
|
||||
Mode mode = Mode::None;
|
||||
|
||||
/// The timer value; this is only used when mode is not None.
|
||||
std::chrono::seconds timer = 0s;
|
||||
};
|
||||
|
||||
/// A Session ID: an x25519 pubkey, with a 05 identifying prefix. On the wire we send just the
|
||||
/// 32-byte pubkey value (i.e. not hex, without the prefix).
|
||||
struct SessionID {
|
||||
/// The fixed session netid, 0x05
|
||||
static constexpr unsigned char netid = 0x05;
|
||||
|
||||
/// The raw x25519 pubkey, as bytes
|
||||
std::array<unsigned char, 32> pubkey;
|
||||
|
||||
/// Returns the full pubkey in hex, including the netid prefix.
|
||||
std::string hex() const;
|
||||
};
|
||||
|
||||
} // namespace session
|
|
@ -1,18 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace session {
|
||||
|
||||
using ustring = std::basic_string<unsigned char>;
|
||||
using ustring_view = std::basic_string_view<unsigned char>;
|
||||
|
||||
namespace config {
|
||||
|
||||
using seqno_t = std::int64_t;
|
||||
|
||||
} // namespace config
|
||||
|
||||
} // namespace session
|
|
@ -1,44 +0,0 @@
|
|||
#pragma once
|
||||
#include "types.hpp"
|
||||
|
||||
namespace session {
|
||||
|
||||
// Helper function to go to/from char pointers to unsigned char pointers:
|
||||
inline const unsigned char* to_unsigned(const char* x) {
|
||||
return reinterpret_cast<const unsigned char*>(x);
|
||||
}
|
||||
inline unsigned char* to_unsigned(char* x) {
|
||||
return reinterpret_cast<unsigned char*>(x);
|
||||
}
|
||||
inline const char* from_unsigned(const unsigned char* x) {
|
||||
return reinterpret_cast<const char*>(x);
|
||||
}
|
||||
inline char* from_unsigned(unsigned char* x) {
|
||||
return reinterpret_cast<char*>(x);
|
||||
}
|
||||
// Helper function to switch between string_view and ustring_view
|
||||
inline ustring_view to_unsigned_sv(std::string_view v) {
|
||||
return {to_unsigned(v.data()), v.size()};
|
||||
}
|
||||
inline std::string_view from_unsigned_sv(ustring_view v) {
|
||||
return {from_unsigned(v.data()), v.size()};
|
||||
}
|
||||
|
||||
/// Returns true if the first string is equal to the second string, compared case-insensitively.
|
||||
inline bool string_iequal(std::string_view s1, std::string_view s2) {
|
||||
return std::equal(s1.begin(), s1.end(), s2.begin(), s2.end(), [](char a, char b) {
|
||||
return std::tolower(static_cast<unsigned char>(a)) ==
|
||||
std::tolower(static_cast<unsigned char>(b));
|
||||
});
|
||||
}
|
||||
|
||||
// C++20 starts_/ends_with backport
|
||||
inline constexpr bool starts_with(std::string_view str, std::string_view prefix) {
|
||||
return str.size() >= prefix.size() && str.substr(prefix.size()) == prefix;
|
||||
}
|
||||
|
||||
inline constexpr bool end_with(std::string_view str, std::string_view suffix) {
|
||||
return str.size() >= suffix.size() && str.substr(str.size() - suffix.size()) == suffix;
|
||||
}
|
||||
|
||||
} // namespace session
|
|
@ -1,19 +0,0 @@
|
|||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/// libsession-util version triplet (major, minor, patch)
|
||||
extern const uint16_t LIBSESSION_UTIL_VERSION[3];
|
||||
|
||||
/// Printable full libsession-util name and version string, such as `libsession-util v0.1.2-release`
|
||||
/// for a tagged release or `libsession-util v0.1.2-7f144eb5` for an untagged build.
|
||||
extern const char* LIBSESSION_UTIL_VERSION_FULL;
|
||||
|
||||
/// Just the version component as a string, e.g. `v0.1.2-release`.
|
||||
extern const char* LIBSESSION_UTIL_VERSION_STR;
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
|
@ -1,34 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/// XEd25519-signed a message given a curve25519 privkey and message. Writes the 64-byte signature
|
||||
/// to `sig` on success and returns 0. Returns non-zero on failure.
|
||||
__attribute__((warn_unused_result)) int session_xed25519_sign(
|
||||
unsigned char* signature /* 64 byte buffer */,
|
||||
const unsigned char* curve25519_privkey /* 32 bytes */,
|
||||
const unsigned char* msg,
|
||||
const unsigned int msg_len);
|
||||
|
||||
/// Verifies an XEd25519-signed message given a 64-byte signature, 32-byte curve25519 pubkey, and
|
||||
/// message. Returns 0 if the signature verifies successfully, non-zero on failure.
|
||||
__attribute__((warn_unused_result)) int session_xed25519_verify(
|
||||
const unsigned char* signature /* 64 bytes */,
|
||||
const unsigned char* pubkey /* 32-bytes */,
|
||||
const unsigned char* msg,
|
||||
const unsigned int msg_len);
|
||||
|
||||
/// Given a curve25519 pubkey, this writes the associated XEd25519-derived Ed25519 pubkey into
|
||||
/// ed25519_pubkey. Note, however, that there are *two* possible Ed25519 pubkeys that could result
|
||||
/// in a given curve25519 pubkey: this always returns the positive value. You can get the other
|
||||
/// possibility (the negative) by flipping the sign bit, i.e. `returned_pubkey[31] |= 0x80`.
|
||||
/// Returns 0 on success, non-0 on failure.
|
||||
__attribute__((warn_unused_result)) int session_xed25519_pubkey(
|
||||
unsigned char* ed25519_pubkey /* 32-byte output buffer */,
|
||||
const unsigned char* curve25519_pubkey /* 32 bytes */);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
|
@ -1,38 +0,0 @@
|
|||
#pragma once
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace session::xed25519 {
|
||||
|
||||
using ustring_view = std::basic_string_view<unsigned char>;
|
||||
|
||||
/// XEd25519-signs a message given the curve25519 privkey and message.
|
||||
std::array<unsigned char, 64> sign(
|
||||
ustring_view curve25519_privkey /* 32 bytes */, ustring_view msg);
|
||||
|
||||
/// "Softer" version that takes and returns strings of regular chars
|
||||
std::string sign(std::string_view curve25519_privkey /* 32 bytes */, std::string_view msg);
|
||||
|
||||
/// Verifies a curve25519 message allegedly signed by the given curve25519 pubkey
|
||||
[[nodiscard]] bool verify(
|
||||
ustring_view signature /* 64 bytes */,
|
||||
ustring_view curve25519_pubkey /* 32 bytes */,
|
||||
ustring_view msg);
|
||||
|
||||
/// "Softer" version that takes strings of regular chars
|
||||
[[nodiscard]] bool verify(
|
||||
std::string_view signature /* 64 bytes */,
|
||||
std::string_view curve25519_pubkey /* 32 bytes */,
|
||||
std::string_view msg);
|
||||
|
||||
/// Given a curve25519 pubkey, this returns the associated XEd25519-derived Ed25519 pubkey. Note,
|
||||
/// however, that there are *two* possible Ed25519 pubkeys that could result in a given curve25519
|
||||
/// pubkey: this always returns the positive value. You can get the other possibility (the
|
||||
/// negative) by flipping the sign bit, i.e. `returned_pubkey[31] |= 0x80`.
|
||||
std::array<unsigned char, 32> pubkey(ustring_view curve25519_pubkey);
|
||||
|
||||
/// "Softer" version that takes/returns strings of regular chars
|
||||
std::string pubkey(std::string_view curve25519_pubkey);
|
||||
|
||||
} // namespace session::xed25519
|
|
@ -8,7 +8,8 @@ import SessionUtilitiesKit
|
|||
|
||||
extension MessageReceiver {
|
||||
internal static func handleLegacyConfigurationMessage(_ db: Database, message: ConfigurationMessage) throws {
|
||||
guard !Features.useSharedUtilForUserConfig else {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard !SessionUtil.userConfigsEnabled else {
|
||||
TopBannerController.show(warning: .outdatedUserConfig)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -260,7 +260,7 @@ internal extension SessionUtil {
|
|||
}
|
||||
convo_info_volatile_set_community(conf, &community)
|
||||
|
||||
case .group: return // TODO: Need to add when the type is added to the lib
|
||||
case .group: return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -419,7 +419,7 @@ public extension SessionUtil {
|
|||
|
||||
return (convoCommunity.last_read > timestampMs)
|
||||
|
||||
case .group: return false // TODO: Need to add when the type is added to the lib
|
||||
case .group: return false
|
||||
}
|
||||
}
|
||||
.defaulting(to: false) // If we don't have a config then just assume it's unread
|
|
@ -42,11 +42,7 @@ internal extension SessionUtil {
|
|||
change: (UnsafeMutablePointer<config_object>?) throws -> ()
|
||||
) throws {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard Features.useSharedUtilForUserConfig else { return }
|
||||
|
||||
// If we haven't completed the required migrations then do nothing (assume that
|
||||
// this is called from a migration change and we won't miss a change)
|
||||
guard SessionUtil.requiredMigrationsCompleted(db) else { return }
|
||||
guard SessionUtil.userConfigsEnabled(db, ignoreRequirementsForRunningMigrations: true) else { return }
|
||||
|
||||
// Since we are doing direct memory manipulation we are using an `Atomic`
|
||||
// type which has blocking access in it's `mutate` closure
|
||||
|
@ -307,7 +303,7 @@ public extension SessionUtil {
|
|||
threadVariant: SessionThread.Variant
|
||||
) -> Bool {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard Features.useSharedUtilForUserConfig else { return true }
|
||||
guard SessionUtil.userConfigsEnabled else { return true }
|
||||
|
||||
let configVariant: ConfigDump.Variant = {
|
||||
switch threadVariant {
|
|
@ -354,7 +354,7 @@ internal extension SessionUtil {
|
|||
}
|
||||
|
||||
// MARK: -- Handle Group Changes
|
||||
// TODO: Add this
|
||||
|
||||
}
|
||||
|
||||
fileprivate static func memberInfo(in legacyGroup: UnsafeMutablePointer<ugroups_legacy_group_info>) -> [String: Bool] {
|
||||
|
@ -708,6 +708,7 @@ public extension SessionUtil {
|
|||
|
||||
static func remove(_ db: Database, groupIds: [String]) throws {
|
||||
guard !groupIds.isEmpty else { return }
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -93,7 +93,7 @@ public extension QueryInterfaceRequest where RowDecoder: FetchableRecord & Table
|
|||
// Then check if any of the changes could affect the config
|
||||
guard
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
Features.useSharedUtilForUserConfig,
|
||||
SessionUtil.userConfigsEnabled(db, ignoreRequirementsForRunningMigrations: true),
|
||||
SessionUtil.assignmentsRequireConfigUpdate(assignments)
|
||||
else { return updatedData }
|
||||
|
|
@ -50,7 +50,7 @@ public enum SessionUtil {
|
|||
/// loaded yet (eg. fresh install)
|
||||
public static var needsSync: Bool {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard Features.useSharedUtilForUserConfig else { return false }
|
||||
guard SessionUtil.userConfigsEnabled else { return false }
|
||||
|
||||
return configStore
|
||||
.wrappedValue
|
||||
|
@ -63,16 +63,44 @@ public enum SessionUtil {
|
|||
|
||||
public static var libSessionVersion: String { String(cString: LIBSESSION_UTIL_VERSION_STR) }
|
||||
|
||||
private static var hasCompletedRequiredMigrations: Bool = false
|
||||
private static let requiredMigrationsCompleted: Atomic<Bool> = Atomic(false)
|
||||
private static let requiredMigrationIdentifiers: Set<String> = [
|
||||
TargetMigrations.Identifier.messagingKit.key(with: _013_SessionUtilChanges.self),
|
||||
TargetMigrations.Identifier.messagingKit.key(with: _014_GenerateInitialUserConfigDumps.self)
|
||||
]
|
||||
|
||||
internal static func requiredMigrationsCompleted(_ db: Database) -> Bool {
|
||||
guard !hasCompletedRequiredMigrations else { return true }
|
||||
public static var userConfigsEnabled: Bool {
|
||||
Features.useSharedUtilForUserConfig &&
|
||||
requiredMigrationsCompleted.wrappedValue
|
||||
}
|
||||
|
||||
internal static func userConfigsEnabled(
|
||||
_ db: Database,
|
||||
ignoreRequirementsForRunningMigrations: Bool
|
||||
) -> Bool {
|
||||
// First check if we are enabled regardless of what we want to ignore
|
||||
guard
|
||||
Features.useSharedUtilForUserConfig,
|
||||
!requiredMigrationsCompleted.wrappedValue,
|
||||
!refreshingUserConfigsEnabled(db),
|
||||
ignoreRequirementsForRunningMigrations,
|
||||
let currentlyRunningMigration: (identifier: TargetMigrations.Identifier, migration: Migration.Type) = Storage.shared.currentlyRunningMigration
|
||||
else { return true }
|
||||
|
||||
let nonIgnoredMigrationIdentifiers: Set<String> = SessionUtil.requiredMigrationIdentifiers
|
||||
.removing(currentlyRunningMigration.identifier.key(with: currentlyRunningMigration.migration))
|
||||
|
||||
return Storage.appliedMigrationIdentifiers(db)
|
||||
.isSuperset(of: [
|
||||
_013_SessionUtilChanges.identifier,
|
||||
_014_GenerateInitialUserConfigDumps.identifier
|
||||
])
|
||||
.isSuperset(of: nonIgnoredMigrationIdentifiers)
|
||||
}
|
||||
|
||||
@discardableResult public static func refreshingUserConfigsEnabled(_ db: Database) -> Bool {
|
||||
let result: Bool = Storage.appliedMigrationIdentifiers(db)
|
||||
.isSuperset(of: SessionUtil.requiredMigrationIdentifiers)
|
||||
|
||||
requiredMigrationsCompleted.mutate { $0 = result }
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
internal static func lastError(_ conf: UnsafeMutablePointer<config_object>?) -> String {
|
||||
|
@ -86,9 +114,6 @@ public enum SessionUtil {
|
|||
userPublicKey: String,
|
||||
ed25519SecretKey: [UInt8]?
|
||||
) {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard Features.useSharedUtilForUserConfig else { return }
|
||||
|
||||
// Ensure we have the ed25519 key and that we haven't already loaded the state before
|
||||
// we continue
|
||||
guard
|
||||
|
@ -104,6 +129,9 @@ public enum SessionUtil {
|
|||
return
|
||||
}
|
||||
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard SessionUtil.userConfigsEnabled(db, ignoreRequirementsForRunningMigrations: true) else { return }
|
||||
|
||||
// Retrieve the existing dumps from the database
|
||||
let existingDumps: Set<ConfigDump> = ((try? ConfigDump.fetchSet(db)) ?? [])
|
||||
let existingDumpVariants: Set<ConfigDump.Variant> = existingDumps
|
||||
|
@ -300,7 +328,7 @@ public enum SessionUtil {
|
|||
|
||||
public static func configHashes(for publicKey: String) -> [String] {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard Features.useSharedUtilForUserConfig else { return [] }
|
||||
guard SessionUtil.userConfigsEnabled else { return [] }
|
||||
|
||||
return Storage.shared
|
||||
.read { db -> [String] in
|
||||
|
@ -347,7 +375,7 @@ public enum SessionUtil {
|
|||
publicKey: String
|
||||
) throws {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard Features.useSharedUtilForUserConfig else { return }
|
||||
guard SessionUtil.userConfigsEnabled else { return }
|
||||
guard !messages.isEmpty else { return }
|
||||
guard !publicKey.isEmpty else { throw MessageReceiverError.noThread }
|
||||
|
|
@ -15,7 +15,8 @@ typedef void (^AppReadyBlock)(void);
|
|||
// This method can be called on any thread.
|
||||
+ (BOOL)isAppReady;
|
||||
|
||||
// This method should only be called on the main thread.
|
||||
// These methods should only be called on the main thread.
|
||||
+ (void)invalidate;
|
||||
+ (void)setAppIsReady;
|
||||
|
||||
// If the app is ready, the block is called immediately;
|
||||
|
|
|
@ -103,6 +103,16 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[self.appDidBecomeReadyBlocks addObject:block];
|
||||
}
|
||||
|
||||
+ (void)invalidate
|
||||
{
|
||||
[self.sharedManager invalidate];
|
||||
}
|
||||
|
||||
- (void)invalidate
|
||||
{
|
||||
self.isAppReady = NO;
|
||||
}
|
||||
|
||||
+ (void)setAppIsReady
|
||||
{
|
||||
[self.sharedManager setAppIsReady];
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public extension Identity {
|
||||
/// The user actually exists very early on during the onboarding process but there are also a few cases
|
||||
/// where we want to know that the user is in a valid state (ie. has completed the proper onboarding
|
||||
/// process), this value indicates that state
|
||||
///
|
||||
/// One case which can happen is if the app crashed during onboarding the user can be left in an invalid
|
||||
/// state (ie. with no display name) - the user would be asked to enter one on a subsequent launch to
|
||||
/// resolve the invalid state
|
||||
static func userCompletedRequiredOnboarding(_ db: Database? = nil) -> Bool {
|
||||
Identity.userExists(db) &&
|
||||
!Profile.fetchOrCreateCurrentUser(db).name.isEmpty
|
||||
}
|
||||
}
|
|
@ -593,7 +593,7 @@ public struct ProfileManager {
|
|||
)
|
||||
}
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
else if !Features.useSharedUtilForUserConfig {
|
||||
else if !SessionUtil.userConfigsEnabled {
|
||||
// If we have a contact record for the profile (ie. it's a synced profile) then
|
||||
// should should send an updated config message, otherwise we should just update
|
||||
// the local state (the shared util has this logic build in to it's handling)
|
||||
|
|
|
@ -22,9 +22,15 @@ open class Storage {
|
|||
return true
|
||||
}
|
||||
|
||||
private let migrationsCompleted: Atomic<Bool> = Atomic(false)
|
||||
internal let internalCurrentlyRunningMigration: Atomic<(identifier: TargetMigrations.Identifier, migration: Migration.Type)?> = Atomic(nil)
|
||||
|
||||
public static let shared: Storage = Storage()
|
||||
public private(set) var isValid: Bool = false
|
||||
public private(set) var hasCompletedMigrations: Bool = false
|
||||
public var hasCompletedMigrations: Bool { migrationsCompleted.wrappedValue }
|
||||
public var currentlyRunningMigration: (identifier: TargetMigrations.Identifier, migration: Migration.Type)? {
|
||||
internalCurrentlyRunningMigration.wrappedValue
|
||||
}
|
||||
public static let defaultPublisherScheduler: ValueObservationScheduler = .async(onQueue: .main)
|
||||
|
||||
fileprivate var dbWriter: DatabaseWriter?
|
||||
|
@ -186,7 +192,7 @@ open class Storage {
|
|||
|
||||
// Store the logic to run when the migration completes
|
||||
let migrationCompleted: (Swift.Result<Void, Error>) -> () = { [weak self] result in
|
||||
self?.hasCompletedMigrations = true
|
||||
self?.migrationsCompleted.mutate { $0 = true }
|
||||
self?.migrationProgressUpdater = nil
|
||||
SUKLegacy.clearLegacyDatabaseInstance()
|
||||
|
||||
|
@ -197,6 +203,12 @@ open class Storage {
|
|||
onComplete(result, needsConfigSync)
|
||||
}
|
||||
|
||||
// Update the 'migrationsCompleted' state (since we not support running migrations when
|
||||
// returning from the background it's possible for this flag to transition back to false)
|
||||
if unperformedMigrations.isEmpty {
|
||||
self.migrationsCompleted.mutate { $0 = false }
|
||||
}
|
||||
|
||||
// Note: The non-async migration should only be used for unit tests
|
||||
guard async else {
|
||||
do { try self.migrator?.migrate(dbWriter) }
|
||||
|
@ -303,7 +315,7 @@ open class Storage {
|
|||
try? SUKLegacy.deleteLegacyDatabaseFilesAndKey()
|
||||
|
||||
Storage.shared.isValid = false
|
||||
Storage.shared.hasCompletedMigrations = false
|
||||
Storage.shared.migrationsCompleted.mutate { $0 = false }
|
||||
Storage.shared.dbWriter = nil
|
||||
|
||||
self.deleteDatabaseFiles()
|
||||
|
|
|
@ -16,7 +16,12 @@ public extension Migration {
|
|||
static func loggedMigrate(_ targetIdentifier: TargetMigrations.Identifier) -> ((_ db: Database) throws -> ()) {
|
||||
return { (db: Database) in
|
||||
SNLogNotTests("[Migration Info] Starting \(targetIdentifier.key(with: self))")
|
||||
try migrate(db)
|
||||
Storage.shared.internalCurrentlyRunningMigration.mutate { $0 = (targetIdentifier, self) }
|
||||
do { try migrate(db) }
|
||||
catch {
|
||||
Storage.shared.internalCurrentlyRunningMigration.mutate { $0 = nil }
|
||||
throw error
|
||||
}
|
||||
SNLogNotTests("[Migration Info] Completed \(targetIdentifier.key(with: self))")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,16 +7,16 @@ import SessionUtilitiesKit
|
|||
import SessionUIKit
|
||||
|
||||
public enum AppSetup {
|
||||
private static var hasRun: Bool = false
|
||||
private static let hasRun: Atomic<Bool> = Atomic(false)
|
||||
|
||||
public static func setupEnvironment(
|
||||
appSpecificBlock: @escaping () -> (),
|
||||
migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil,
|
||||
migrationsCompletion: @escaping (Result<Void, Error>, Bool) -> ()
|
||||
) {
|
||||
guard !AppSetup.hasRun else { return }
|
||||
guard !AppSetup.hasRun.wrappedValue else { return }
|
||||
|
||||
AppSetup.hasRun = true
|
||||
AppSetup.hasRun.mutate { $0 = true }
|
||||
|
||||
var backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(labelStr: #function)
|
||||
|
||||
|
@ -84,6 +84,12 @@ public enum AppSetup {
|
|||
)
|
||||
}
|
||||
|
||||
// Refresh the migration state for 'SessionUtil' so it's logic can start running
|
||||
// correctly when called (doing this here instead of automatically via the
|
||||
// `SessionUtil.userConfigsEnabled` property to avoid having to use the correct
|
||||
// method when calling within a database read/write closure)
|
||||
Storage.shared.read { db in SessionUtil.refreshingUserConfigsEnabled(db) }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
migrationsCompletion(result, (needsConfigSync || SessionUtil.needsSync))
|
||||
|
||||
|
|
Loading…
Reference in New Issue