mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-10 09:18:13 +00:00
core, ui: badge credential details in developer tools (#7527)
This commit is contained in:
@@ -215,10 +215,11 @@ data BadgeAlertPrice = BadgeAlertPrice
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
-- | The user's badge as the badge surfaces render it. The purchase keys are deliberately absent:
|
||||
-- this travels to the UI and over remote control, and they are secrets that stay in core.
|
||||
-- | The user's badge as the badge surfaces render it. The private purchase key is deliberately
|
||||
-- absent: this travels to the UI and over remote control, and it is a secret that stays in core.
|
||||
data BadgeState = BadgeState
|
||||
{ badgePurchaseId :: Int64,
|
||||
purchaseKey :: C.PublicKeyEd25519, -- the purchase's identifier on the service
|
||||
badgeType :: BadgeType,
|
||||
shown :: BoolDef,
|
||||
monthsLeft :: Int,
|
||||
|
||||
@@ -84,7 +84,7 @@ import qualified Simplex.Messaging.Agent.Store.DB as DB
|
||||
import Simplex.Messaging.Client (HostMode (..), SMPProxyFallback (..), SMPProxyMode (..), SMPWebPortServers (..), SocksMode (..))
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Chat.Badges (BadgeCredential, FileSizeLimits, LocalBadge)
|
||||
import Simplex.Chat.Badges.Service (BadgeServiceErrorCode)
|
||||
import Simplex.Chat.Badges.Service (BadgeServiceErrorCode, StatementEntry)
|
||||
import Simplex.Chat.Badges.Types (BadgeAlert (..), BadgeAlertKind, BadgeState (..))
|
||||
import Simplex.Messaging.Crypto.BBS (BBSPublicKey)
|
||||
import Simplex.Messaging.Crypto.File (CryptoFile (..))
|
||||
@@ -660,6 +660,7 @@ data ChatCommand
|
||||
| AddBadge BadgeCredential -- attach an issued badge credential (testing; credential from `simplex-chat badge sign`)
|
||||
| APIRedeemBadgeCode {userId :: UserId, code :: Text} -- redeem a badge code with the configured badge service
|
||||
| APIGetBadgeState {userId :: UserId} -- the user's badges, their balances and any current alert
|
||||
| APIGetBadgeLedger {userId :: UserId, badgePurchaseId :: Int64} -- the purchase's ledger, oldest first
|
||||
-- episode is last because it is free text: it is the value that makes one occurrence of an
|
||||
-- alert distinct from the next, and the app returns whatever it was given
|
||||
| APIAckBadgeAlert {userId :: UserId, badgePurchaseId :: Int64, alertKind :: BadgeAlertKind, snooze :: Bool, episode :: Text}
|
||||
@@ -869,6 +870,7 @@ data ChatResponse
|
||||
| CRServiceReplyAccepted {user :: User, connectionId :: AgentConnId}
|
||||
| CRBadgeRedeemed {user :: User, redeemedBadge :: LocalBadge, newBadge :: Bool, badgeState :: Maybe BadgeState}
|
||||
| CRBadgeState {user :: User, badgeState :: Maybe BadgeState}
|
||||
| CRBadgeLedger {user :: User, badgeLedger :: [StatementEntry]}
|
||||
| CRUserAcceptedGroupSent {user :: User, groupInfo :: GroupInfo, hostContact :: Maybe Contact}
|
||||
| CRUserDeletedMembers {user :: User, groupInfo :: GroupInfo, members :: [GroupMember], withMessages :: Bool, msgSigned :: Bool}
|
||||
| CRGroupsList {user :: User, groups :: [GroupInfo]}
|
||||
|
||||
@@ -3567,6 +3567,8 @@ processChatCommand cxt nm = \case
|
||||
-- the read also signals the worker, whose results follow as CEvtBadgeChanged
|
||||
lift $ startBadgeWork user
|
||||
CRBadgeState user <$> getUserBadgeState user
|
||||
APIGetBadgeLedger userId badgePurchaseId -> withUserId userId $ \user ->
|
||||
CRBadgeLedger user <$> withStore' (\db -> getBadgeLedger db user badgePurchaseId)
|
||||
APIAckBadgeAlert userId badgePurchaseId alertKind snooze episode -> withUserId userId $ \user -> do
|
||||
now <- badgeNow
|
||||
let snoozeUntil = if snooze then Just (addUTCTime nominalDay now) else Nothing
|
||||
@@ -5417,9 +5419,10 @@ getUserBadgeState user = do
|
||||
Just p@UserBadgePurchase {badgePurchaseId} ->
|
||||
fmap (badgeStateOf now p) <$> withStore' (`getBadgeLedgerLastEntry` badgePurchaseId)
|
||||
where
|
||||
badgeStateOf now p@UserBadgePurchase {badgePurchaseId, badgeType, shown} balance =
|
||||
badgeStateOf now p@UserBadgePurchase {badgePurchaseId, purchaseKey, badgeType, shown} balance =
|
||||
BadgeState
|
||||
{ badgePurchaseId,
|
||||
purchaseKey,
|
||||
badgeType,
|
||||
shown = BoolDef shown,
|
||||
monthsLeft = balanceMonths balance,
|
||||
@@ -6039,6 +6042,7 @@ chatCommandP =
|
||||
"/_service_request " *> (APISendServiceRequest <$> A.decimal <* A.space <*> strP <*> optional (" timeout=" *> (realToFrac <$> A.double)) <*> optional (" sign_key=" *> strP) <* A.space <*> jsonP),
|
||||
"/_redeem_badge_code " *> (APIRedeemBadgeCode <$> A.decimal <* A.space <*> textP),
|
||||
"/_badge state " *> (APIGetBadgeState <$> A.decimal),
|
||||
"/_badge ledger " *> (APIGetBadgeLedger <$> A.decimal <* A.space <*> A.decimal),
|
||||
"/_badge ack " *> (APIAckBadgeAlert <$> A.decimal <* A.space <*> A.decimal <* A.space <*> badgeAlertKindP <* A.space <*> onOffP <* A.space <*> textP),
|
||||
"/_service_response " *> (APISendServiceResponse <$> A.decimal <* A.space <*> strP <* A.space <*> jsonP),
|
||||
"/_call invite @" *> (APISendCallInvitation <$> A.decimal <* A.space <*> jsonP),
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
|
||||
module Simplex.Chat.Store.Badges
|
||||
( BadgeCodeRedemption (..),
|
||||
@@ -22,6 +23,7 @@ module Simplex.Chat.Store.Badges
|
||||
getLatestIssuedCredential,
|
||||
storeBadgeStatement,
|
||||
getBadgeLedgerLastEntry,
|
||||
getBadgeLedger,
|
||||
getBadgeLedgerEntryId,
|
||||
)
|
||||
where
|
||||
@@ -31,7 +33,7 @@ import Crypto.Random (ChaChaDRG)
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Int (Int64)
|
||||
import Data.Maybe (isJust)
|
||||
import Data.Maybe (isJust, mapMaybe)
|
||||
import Data.Text (Text)
|
||||
import Data.Time.Clock (UTCTime)
|
||||
import Simplex.Chat.Badges
|
||||
@@ -306,7 +308,7 @@ storeBadgeStatement db badgePurchaseId badgeType tip entries now =
|
||||
-- | The balance is the last row; nothing derives it by summing the history.
|
||||
getBadgeLedgerLastEntry :: DB.Connection -> Int64 -> IO (Maybe StatementEntry)
|
||||
getBadgeLedgerLastEntry db badgePurchaseId =
|
||||
maybeFirstRow' Nothing toEntry $
|
||||
maybeFirstRow' Nothing toStatementEntry $
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
@@ -318,10 +320,27 @@ getBadgeLedgerLastEntry db badgePurchaseId =
|
||||
LIMIT 1
|
||||
|]
|
||||
(Only badgePurchaseId)
|
||||
where
|
||||
toEntry ((entryId, changeMonths, balanceMonths, balanceStartTs, balanceAnchorTs, balanceBadgeType) :. (wasPausedSince, createdAt, entryType_, credit_, debit_, value_)) =
|
||||
(\entryType -> StatementEntry {entryId, changeMonths, balanceMonths, balanceStartTs, balanceAnchorTs, balanceBadgeType, wasPausedSince, createdAt, entryType})
|
||||
<$> maybe (entryTypeFromColumns entryType_ credit_ debit_) (entryTypeFromValue entryType_) value_
|
||||
|
||||
-- | Oldest first. A row whose type this version cannot rebuild is left out, as it is from the tip.
|
||||
getBadgeLedger :: DB.Connection -> User -> Int64 -> IO [StatementEntry]
|
||||
getBadgeLedger db User {userId} badgePurchaseId =
|
||||
mapMaybe toStatementEntry
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT l.entry_uuid, l.change_months, l.balance_months, l.balance_start_ts, l.balance_anchor_ts, l.balance_badge_type,
|
||||
l.was_paused_since, l.service_created_at, l.entry_type, l.entry_credit_type, l.entry_debit_type, l.entry_type_value
|
||||
FROM badge_ledger l
|
||||
JOIN badge_purchases p ON p.badge_purchase_id = l.badge_purchase_id
|
||||
WHERE l.badge_purchase_id = ? AND p.user_id = ?
|
||||
ORDER BY l.entry_id
|
||||
|]
|
||||
(badgePurchaseId, userId)
|
||||
|
||||
toStatementEntry :: (Text, Int, Int, UTCTime, UTCTime, BadgeType) :. (Maybe UTCTime, UTCTime, Text, Maybe Text, Maybe Text, Maybe Text) -> Maybe StatementEntry
|
||||
toStatementEntry ((entryId, changeMonths, balanceMonths, balanceStartTs, balanceAnchorTs, balanceBadgeType) :. (wasPausedSince, createdAt, entryType_, credit_, debit_, value_)) =
|
||||
(\entryType -> StatementEntry {entryId, changeMonths, balanceMonths, balanceStartTs, balanceAnchorTs, balanceBadgeType, wasPausedSince, createdAt, entryType})
|
||||
<$> maybe (entryTypeFromColumns entryType_ credit_ debit_) (entryTypeFromValue entryType_) value_
|
||||
|
||||
-- | Decodes the stored JSON rather than rebuilding from the tag, so a version that has since
|
||||
-- learnt the type reads it with its fields, and one that has not still gets it back verbatim.
|
||||
|
||||
@@ -4024,6 +4024,18 @@ Query:
|
||||
Plan:
|
||||
SCAN group_members
|
||||
|
||||
Query:
|
||||
SELECT l.entry_uuid, l.change_months, l.balance_months, l.balance_start_ts, l.balance_anchor_ts, l.balance_badge_type,
|
||||
l.was_paused_since, l.service_created_at, l.entry_type, l.entry_credit_type, l.entry_debit_type, l.entry_type_value
|
||||
FROM badge_ledger l
|
||||
JOIN badge_purchases p ON p.badge_purchase_id = l.badge_purchase_id
|
||||
WHERE l.badge_purchase_id = ? AND p.user_id = ?
|
||||
ORDER BY l.entry_id
|
||||
|
||||
Plan:
|
||||
SEARCH p USING COVERING INDEX idx_badge_purchases_user (user_id=? AND rowid=?)
|
||||
SEARCH l USING INDEX idx_badge_ledger_purchase (badge_purchase_id=?)
|
||||
|
||||
Query:
|
||||
SELECT m.group_member_id
|
||||
FROM group_members m
|
||||
@@ -7679,6 +7691,10 @@ Query: SELECT sent_inv_queue_info FROM group_members WHERE group_member_id = ? A
|
||||
Plan:
|
||||
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query: SELECT service_created_at, balance_start_ts FROM badge_ledger ORDER BY entry_id
|
||||
Plan:
|
||||
SCAN badge_ledger
|
||||
|
||||
Query: SELECT shared_msg_id FROM chat_items WHERE shared_msg_id IS NOT NULL ORDER BY chat_item_id DESC LIMIT 1
|
||||
Plan:
|
||||
SCAN chat_items
|
||||
|
||||
@@ -44,6 +44,8 @@ import Simplex.Chat.Help
|
||||
import Simplex.Chat.Library.Commands (badgeServiceErrorText, maxImageSize)
|
||||
import Simplex.Chat.Markdown
|
||||
import Simplex.Chat.Badges (BadgeInfo (..), BadgeStatus (..), BadgeType (..), LocalBadge, localBadgeInfo, localBadgeStatus)
|
||||
import Simplex.Chat.Badges.Ledger (creditTypeTag, debitTypeTag)
|
||||
import Simplex.Chat.Badges.Service (StatementEntry (..), StatementEntryType (..))
|
||||
import Simplex.Chat.Badges.Types (BadgeAlert (..), BadgeState (..))
|
||||
import Simplex.Chat.Messages hiding (NewChatItem (..))
|
||||
import Simplex.Chat.Messages.CIContent
|
||||
@@ -192,6 +194,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, showFullLinks, te
|
||||
-- the badge is only shown when it is the one now on the profile; a replayed code's badge may not be
|
||||
CRBadgeRedeemed u badge newBadge _ -> ttyUser u $ if newBadge then "badge redeemed" : viewContactBadge (Just badge) else ["badge already redeemed"]
|
||||
CRBadgeState u st -> ttyUser u $ viewUserBadgeState st
|
||||
CRBadgeLedger u entries -> ttyUser u $ viewBadgeLedger entries
|
||||
CRGroupCreated u g -> ttyUser u $ viewGroupCreated g testView
|
||||
CRPublicGroupCreated u g _groupLink _relays -> ttyUser u $ viewGroupCreated g testView
|
||||
CRPublicGroupCreationFailed u results -> ttyUser u $ viewPublicGroupCreationFailed results
|
||||
@@ -1854,6 +1857,17 @@ viewUserBadgeState = maybe [] viewBadge
|
||||
viewBadgeAlert :: BadgeAlert -> [StyledString]
|
||||
viewBadgeAlert BadgeAlert {kind, date} = [plain $ "badge alert: " <> textEncode kind <> " " <> day date]
|
||||
|
||||
viewBadgeLedger :: [StatementEntry] -> [StyledString]
|
||||
viewBadgeLedger [] = ["no ledger entries"]
|
||||
viewBadgeLedger entries = map viewEntry entries
|
||||
where
|
||||
viewEntry StatementEntry {createdAt, entryType, changeMonths, balanceMonths, balanceStartTs} =
|
||||
plain $ day createdAt <> " " <> entryKind entryType <> " " <> withSign changeMonths <> " -> " <> tshow balanceMonths <> ", from " <> day balanceStartTs
|
||||
entryKind = \case
|
||||
SECredit c -> creditTypeTag c
|
||||
SEDebit d -> debitTypeTag d
|
||||
withSign n = (if n >= 0 then "+" else "") <> tshow n
|
||||
|
||||
day :: UTCTime -> Text
|
||||
day = T.pack . formatTime defaultTimeLocale "%Y-%m-%d"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user