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:
Morgan Pretty 2023-04-21 16:34:06 +10:00
parent ad3e53d235
commit 6fd574916b
72 changed files with 426 additions and 3929 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "LibSession-Util"]
path = LibSession-Util
url = git@github.com:oxen-io/libsession-util.git

1
LibSession-Util Submodule

@ -0,0 +1 @@
Subproject commit 53c824de0d514307f3bad6a62449166bd10da6f8

View File

@ -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"

View File

@ -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;

View File

@ -5,6 +5,24 @@
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Build libSession"
scriptText = "&quot;${SRCROOT}/Scripts/build_libSession_util.sh&quot;&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D221A088169C9E5E00537ABF"
BuildableName = "Session.app"
BlueprintName = "Session"
ReferencedContainer = "container:Session.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"

View File

@ -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 = "&quot;${SRCROOT}/Scripts/build_libSession_util.sh&quot;&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C3C2A6EF25539DE700C340D1"
BuildableName = "SessionMessagingKit.framework"
BlueprintName = "SessionMessagingKit"
ReferencedContainer = "container:Session.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"

View File

@ -6,6 +6,24 @@
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Build libSession"
scriptText = "&quot;${SRCROOT}/Scripts/build_libSession_util.sh&quot;&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7BC01A3A241F40AB00BC7C55"
BuildableName = "SessionNotificationServiceExtension.appex"
BlueprintName = "SessionNotificationServiceExtension"
ReferencedContainer = "container:Session.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"

View File

@ -6,6 +6,24 @@
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Build libSession"
scriptText = "&quot;${SRCROOT}/Scripts/build_libSession_util.sh&quot;&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "453518671FC635DD00210559"
BuildableName = "SessionShareExtension.appex"
BlueprintName = "SessionShareExtension"
ReferencedContainer = "container:Session.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"

View File

@ -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 = "&quot;${SRCROOT}/Scripts/build_libSession_util.sh&quot;&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C3C2A678255388CC00C340D1"
BuildableName = "SessionUtilitiesKit.framework"
BlueprintName = "SessionUtilitiesKit"
ReferencedContainer = "container:Session.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"

View File

@ -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 = "&quot;${SRCROOT}/Scripts/build_libSession_util.sh&quot;&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C33FD9AA255A548A00E217F9"
BuildableName = "SignalUtilitiesKit.framework"
BlueprintName = "SignalUtilitiesKit"
ReferencedContainer = "container:Session.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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?)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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>

View File

@ -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 *
}
}

View File

@ -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

View File

@ -1,13 +0,0 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
typedef int64_t seqno_t;
#ifdef __cplusplus
}
#endif

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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 };
}

View File

@ -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

View File

@ -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;

View File

@ -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`
};
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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 {

View File

@ -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 }
}
}

View File

@ -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 }

View File

@ -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 }

View File

@ -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;

View File

@ -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];

View File

@ -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
}
}

View File

@ -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)

View File

@ -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()

View File

@ -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))")
}
}

View File

@ -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))