store and use notifications token

This commit is contained in:
boufni95 2025-07-23 15:45:42 +00:00
parent 5ee048a568
commit c18f71c548
27 changed files with 564 additions and 28 deletions

View file

@ -18,6 +18,7 @@ import { InviteToken } from "./build/src/services/storage/entity/InviteToken.js"
import { DebitAccess } from "./build/src/services/storage/entity/DebitAccess.js" import { DebitAccess } from "./build/src/services/storage/entity/DebitAccess.js"
import { UserOffer } from "./build/src/services/storage/entity/UserOffer.js" import { UserOffer } from "./build/src/services/storage/entity/UserOffer.js"
import { ManagementGrant } from "./build/src/services/storage/entity/ManagementGrant.js" import { ManagementGrant } from "./build/src/services/storage/entity/ManagementGrant.js"
import { AppUserDevice } from "./build/src/services/storage/entity/AppUserDevice.js"
import { Initial1703170309875 } from './build/src/services/storage/migrations/1703170309875-initial.js' import { Initial1703170309875 } from './build/src/services/storage/migrations/1703170309875-initial.js'
import { LspOrder1718387847693 } from './build/src/services/storage/migrations/1718387847693-lsp_order.js' import { LspOrder1718387847693 } from './build/src/services/storage/migrations/1718387847693-lsp_order.js'
@ -40,7 +41,8 @@ export default new DataSource({
PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043,
UserOffer1733502626042, ManagementGrant1751307732346, InvoiceCallbackUrls1752425992291], UserOffer1733502626042, ManagementGrant1751307732346, InvoiceCallbackUrls1752425992291],
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment, entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment,
UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo, TrackedProvider, InviteToken, DebitAccess, UserOffer, ManagementGrant], UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo,
TrackedProvider, InviteToken, DebitAccess, UserOffer, ManagementGrant, AppUserDevice],
// synchronize: true, // synchronize: true,
}) })
//npx typeorm migration:generate ./src/services/storage/migrations/management_grant_banned -d ./datasource.js //npx typeorm migration:generate ./src/services/storage/migrations/app_user_device -d ./datasource.js

View file

@ -93,6 +93,11 @@ The nostr server will send back a message response, and inside the body there wi
- input: [EnrollAdminTokenRequest](#EnrollAdminTokenRequest) - input: [EnrollAdminTokenRequest](#EnrollAdminTokenRequest)
- This methods has an __empty__ __response__ body - This methods has an __empty__ __response__ body
- EnrollMessagingToken
- auth type: __User__
- input: [MessagingToken](#MessagingToken)
- This methods has an __empty__ __response__ body
- GetAppsMetrics - GetAppsMetrics
- auth type: __Metrics__ - auth type: __Metrics__
- input: [AppsMetricsRequest](#AppsMetricsRequest) - input: [AppsMetricsRequest](#AppsMetricsRequest)
@ -515,6 +520,13 @@ The nostr server will send back a message response, and inside the body there wi
- input: [EnrollAdminTokenRequest](#EnrollAdminTokenRequest) - input: [EnrollAdminTokenRequest](#EnrollAdminTokenRequest)
- This methods has an __empty__ __response__ body - This methods has an __empty__ __response__ body
- EnrollMessagingToken
- auth type: __User__
- http method: __post__
- http route: __/api/user/messaging/enroll__
- input: [MessagingToken](#MessagingToken)
- This methods has an __empty__ __response__ body
- GetApp - GetApp
- auth type: __App__ - auth type: __App__
- http method: __post__ - http method: __post__
@ -1360,6 +1372,10 @@ The nostr server will send back a message response, and inside the body there wi
### ManageOperation ### ManageOperation
- __npub__: _string_ - __npub__: _string_
### MessagingToken
- __device_id__: _string_
- __firebase_messaging_token__: _string_
### MetricsFile ### MetricsFile
### MigrationUpdate ### MigrationUpdate

View file

@ -74,6 +74,7 @@ type Client struct {
EditDebit func(req DebitAuthorizationRequest) error EditDebit func(req DebitAuthorizationRequest) error
EncryptionExchange func(req EncryptionExchangeRequest) error EncryptionExchange func(req EncryptionExchangeRequest) error
EnrollAdminToken func(req EnrollAdminTokenRequest) error EnrollAdminToken func(req EnrollAdminTokenRequest) error
EnrollMessagingToken func(req MessagingToken) error
GetApp func() (*Application, error) GetApp func() (*Application, error)
GetAppUser func(req GetAppUserRequest) (*AppUser, error) GetAppUser func(req GetAppUserRequest) (*AppUser, error)
GetAppUserLNURLInfo func(req GetAppUserLNURLInfoRequest) (*LnurlPayInfoResponse, error) GetAppUserLNURLInfo func(req GetAppUserLNURLInfoRequest) (*LnurlPayInfoResponse, error)
@ -667,6 +668,30 @@ func NewClient(params ClientParams) *Client {
} }
return nil return nil
}, },
EnrollMessagingToken: func(req MessagingToken) error {
auth, err := params.RetrieveUserAuth()
if err != nil {
return err
}
finalRoute := "/api/user/messaging/enroll"
body, err := json.Marshal(req)
if err != nil {
return err
}
resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth)
if err != nil {
return err
}
result := ResultError{}
err = json.Unmarshal(resBody, &result)
if err != nil {
return err
}
if result.Status == "ERROR" {
return fmt.Errorf(result.Reason)
}
return nil
},
GetApp: func() (*Application, error) { GetApp: func() (*Application, error) {
auth, err := params.RetrieveAppAuth() auth, err := params.RetrieveAppAuth()
if err != nil { if err != nil {

View file

@ -445,6 +445,10 @@ type ManageAuthorizations struct {
type ManageOperation struct { type ManageOperation struct {
Npub string `json:"npub"` Npub string `json:"npub"`
} }
type MessagingToken struct {
Device_id string `json:"device_id"`
Firebase_messaging_token string `json:"firebase_messaging_token"`
}
type MetricsFile struct { type MetricsFile struct {
} }
type MigrationUpdate struct { type MigrationUpdate struct {

View file

@ -427,6 +427,18 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
} }
break break
case 'EnrollMessagingToken':
if (!methods.EnrollMessagingToken) {
throw new Error('method EnrollMessagingToken not found' )
} else {
const error = Types.MessagingTokenValidate(operation.req)
opStats.validate = process.hrtime.bigint()
if (error !== null) throw error
await methods.EnrollMessagingToken({...operation, ctx}); responses.push({ status: 'OK' })
opStats.handle = process.hrtime.bigint()
callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
}
break
case 'GetDebitAuthorizations': case 'GetDebitAuthorizations':
if (!methods.GetDebitAuthorizations) { if (!methods.GetDebitAuthorizations) {
throw new Error('method GetDebitAuthorizations not found' ) throw new Error('method GetDebitAuthorizations not found' )
@ -847,6 +859,28 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
}) })
if (!opts.allowNotImplementedMethods && !methods.EnrollMessagingToken) throw new Error('method: EnrollMessagingToken is not implemented')
app.post('/api/user/messaging/enroll', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'EnrollMessagingToken', batch: false, nostr: false, batchSize: 0}
const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n }
let authCtx: Types.AuthContext = {}
try {
if (!methods.EnrollMessagingToken) throw new Error('method: EnrollMessagingToken is not implemented')
const authContext = await opts.UserAuthGuard(req.headers['authorization'])
authCtx = authContext
stats.guard = process.hrtime.bigint()
const request = req.body
const error = Types.MessagingTokenValidate(request)
stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback)
const query = req.query
const params = req.params
await methods.EnrollMessagingToken({rpcName:'EnrollMessagingToken', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint()
res.json({status: 'OK'})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.GetApp) throw new Error('method: GetApp is not implemented') if (!opts.allowNotImplementedMethods && !methods.GetApp) throw new Error('method: GetApp is not implemented')
app.post('/api/app/get', async (req, res) => { app.post('/api/app/get', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetApp', batch: false, nostr: false, batchSize: 0} const info: Types.RequestInfo = { rpcName: 'GetApp', batch: false, nostr: false, batchSize: 0}

View file

@ -276,6 +276,17 @@ export default (params: ClientParams) => ({
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
EnrollMessagingToken: async (request: Types.MessagingToken): Promise<ResultError | ({ status: 'OK' })> => {
const auth = await params.retrieveUserAuth()
if (auth === null) throw new Error('retrieveUserAuth() returned null')
let finalRoute = '/api/user/messaging/enroll'
const { data } = await axios.post(params.baseUrl + finalRoute, request, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
return data
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetApp: async (): Promise<ResultError | ({ status: 'OK' }& Types.Application)> => { GetApp: async (): Promise<ResultError | ({ status: 'OK' }& Types.Application)> => {
const auth = await params.retrieveAppAuth() const auth = await params.retrieveAppAuth()
if (auth === null) throw new Error('retrieveAppAuth() returned null') if (auth === null) throw new Error('retrieveAppAuth() returned null')

View file

@ -233,6 +233,18 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
EnrollMessagingToken: async (request: Types.MessagingToken): Promise<ResultError | ({ status: 'OK' })> => {
const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {}
nostrRequest.body = request
const data = await send(params.pubDestination, {rpcName:'EnrollMessagingToken',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
return data
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetAppsMetrics: async (request: Types.AppsMetricsRequest): Promise<ResultError | ({ status: 'OK' }& Types.AppsMetrics)> => { GetAppsMetrics: async (request: Types.AppsMetricsRequest): Promise<ResultError | ({ status: 'OK' }& Types.AppsMetrics)> => {
const auth = await params.retrieveNostrMetricsAuth() const auth = await params.retrieveNostrMetricsAuth()
if (auth === null) throw new Error('retrieveNostrMetricsAuth() returned null') if (auth === null) throw new Error('retrieveNostrMetricsAuth() returned null')

View file

@ -303,6 +303,18 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
} }
break break
case 'EnrollMessagingToken':
if (!methods.EnrollMessagingToken) {
throw new Error('method not defined: EnrollMessagingToken')
} else {
const error = Types.MessagingTokenValidate(operation.req)
opStats.validate = process.hrtime.bigint()
if (error !== null) throw error
await methods.EnrollMessagingToken({...operation, ctx}); responses.push({ status: 'OK' })
opStats.handle = process.hrtime.bigint()
callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
}
break
case 'GetDebitAuthorizations': case 'GetDebitAuthorizations':
if (!methods.GetDebitAuthorizations) { if (!methods.GetDebitAuthorizations) {
throw new Error('method not defined: GetDebitAuthorizations') throw new Error('method not defined: GetDebitAuthorizations')
@ -665,6 +677,22 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
}catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case 'EnrollMessagingToken':
try {
if (!methods.EnrollMessagingToken) throw new Error('method: EnrollMessagingToken is not implemented')
const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint()
authCtx = authContext
const request = req.body
const error = Types.MessagingTokenValidate(request)
stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback)
await methods.EnrollMessagingToken({rpcName:'EnrollMessagingToken', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint()
res({status: 'OK'})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
}catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break
case 'GetAppsMetrics': case 'GetAppsMetrics':
try { try {
if (!methods.GetAppsMetrics) throw new Error('method: GetAppsMetrics is not implemented') if (!methods.GetAppsMetrics) throw new Error('method: GetAppsMetrics is not implemented')

View file

@ -35,8 +35,8 @@ export type UserContext = {
app_user_id: string app_user_id: string
user_id: string user_id: string
} }
export type UserMethodInputs = AddProduct_Input | AddUserOffer_Input | AuthorizeDebit_Input | AuthorizeManage_Input | BanDebit_Input | DecodeInvoice_Input | DeleteUserOffer_Input | EditDebit_Input | EnrollAdminToken_Input | GetDebitAuthorizations_Input | GetHttpCreds_Input | GetLNURLChannelLink_Input | GetLnurlPayLink_Input | GetLnurlWithdrawLink_Input | GetManageAuthorizations_Input | GetPaymentState_Input | GetUserInfo_Input | GetUserOffer_Input | GetUserOfferInvoices_Input | GetUserOffers_Input | GetUserOperations_Input | NewAddress_Input | NewInvoice_Input | NewProductInvoice_Input | PayAddress_Input | PayInvoice_Input | ResetDebit_Input | ResetManage_Input | RespondToDebit_Input | UpdateCallbackUrl_Input | UpdateUserOffer_Input | UserHealth_Input export type UserMethodInputs = AddProduct_Input | AddUserOffer_Input | AuthorizeDebit_Input | AuthorizeManage_Input | BanDebit_Input | DecodeInvoice_Input | DeleteUserOffer_Input | EditDebit_Input | EnrollAdminToken_Input | EnrollMessagingToken_Input | GetDebitAuthorizations_Input | GetHttpCreds_Input | GetLNURLChannelLink_Input | GetLnurlPayLink_Input | GetLnurlWithdrawLink_Input | GetManageAuthorizations_Input | GetPaymentState_Input | GetUserInfo_Input | GetUserOffer_Input | GetUserOfferInvoices_Input | GetUserOffers_Input | GetUserOperations_Input | NewAddress_Input | NewInvoice_Input | NewProductInvoice_Input | PayAddress_Input | PayInvoice_Input | ResetDebit_Input | ResetManage_Input | RespondToDebit_Input | UpdateCallbackUrl_Input | UpdateUserOffer_Input | UserHealth_Input
export type UserMethodOutputs = AddProduct_Output | AddUserOffer_Output | AuthorizeDebit_Output | AuthorizeManage_Output | BanDebit_Output | DecodeInvoice_Output | DeleteUserOffer_Output | EditDebit_Output | EnrollAdminToken_Output | GetDebitAuthorizations_Output | GetHttpCreds_Output | GetLNURLChannelLink_Output | GetLnurlPayLink_Output | GetLnurlWithdrawLink_Output | GetManageAuthorizations_Output | GetPaymentState_Output | GetUserInfo_Output | GetUserOffer_Output | GetUserOfferInvoices_Output | GetUserOffers_Output | GetUserOperations_Output | NewAddress_Output | NewInvoice_Output | NewProductInvoice_Output | PayAddress_Output | PayInvoice_Output | ResetDebit_Output | ResetManage_Output | RespondToDebit_Output | UpdateCallbackUrl_Output | UpdateUserOffer_Output | UserHealth_Output export type UserMethodOutputs = AddProduct_Output | AddUserOffer_Output | AuthorizeDebit_Output | AuthorizeManage_Output | BanDebit_Output | DecodeInvoice_Output | DeleteUserOffer_Output | EditDebit_Output | EnrollAdminToken_Output | EnrollMessagingToken_Output | GetDebitAuthorizations_Output | GetHttpCreds_Output | GetLNURLChannelLink_Output | GetLnurlPayLink_Output | GetLnurlWithdrawLink_Output | GetManageAuthorizations_Output | GetPaymentState_Output | GetUserInfo_Output | GetUserOffer_Output | GetUserOfferInvoices_Output | GetUserOffers_Output | GetUserOperations_Output | NewAddress_Output | NewInvoice_Output | NewProductInvoice_Output | PayAddress_Output | PayInvoice_Output | ResetDebit_Output | ResetManage_Output | RespondToDebit_Output | UpdateCallbackUrl_Output | UpdateUserOffer_Output | UserHealth_Output
export type AuthContext = AdminContext | AppContext | GuestContext | GuestWithPubContext | MetricsContext | UserContext export type AuthContext = AdminContext | AppContext | GuestContext | GuestWithPubContext | MetricsContext | UserContext
export type AddApp_Input = {rpcName:'AddApp', req: AddAppRequest} export type AddApp_Input = {rpcName:'AddApp', req: AddAppRequest}
@ -99,6 +99,9 @@ export type EncryptionExchange_Output = ResultError | { status: 'OK' }
export type EnrollAdminToken_Input = {rpcName:'EnrollAdminToken', req: EnrollAdminTokenRequest} export type EnrollAdminToken_Input = {rpcName:'EnrollAdminToken', req: EnrollAdminTokenRequest}
export type EnrollAdminToken_Output = ResultError | { status: 'OK' } export type EnrollAdminToken_Output = ResultError | { status: 'OK' }
export type EnrollMessagingToken_Input = {rpcName:'EnrollMessagingToken', req: MessagingToken}
export type EnrollMessagingToken_Output = ResultError | { status: 'OK' }
export type GetApp_Input = {rpcName:'GetApp'} export type GetApp_Input = {rpcName:'GetApp'}
export type GetApp_Output = ResultError | ({ status: 'OK' } & Application) export type GetApp_Output = ResultError | ({ status: 'OK' } & Application)
@ -342,6 +345,7 @@ export type ServerMethods = {
EditDebit?: (req: EditDebit_Input & {ctx: UserContext }) => Promise<void> EditDebit?: (req: EditDebit_Input & {ctx: UserContext }) => Promise<void>
EncryptionExchange?: (req: EncryptionExchange_Input & {ctx: GuestContext }) => Promise<void> EncryptionExchange?: (req: EncryptionExchange_Input & {ctx: GuestContext }) => Promise<void>
EnrollAdminToken?: (req: EnrollAdminToken_Input & {ctx: UserContext }) => Promise<void> EnrollAdminToken?: (req: EnrollAdminToken_Input & {ctx: UserContext }) => Promise<void>
EnrollMessagingToken?: (req: EnrollMessagingToken_Input & {ctx: UserContext }) => Promise<void>
GetApp?: (req: GetApp_Input & {ctx: AppContext }) => Promise<Application> GetApp?: (req: GetApp_Input & {ctx: AppContext }) => Promise<Application>
GetAppUser?: (req: GetAppUser_Input & {ctx: AppContext }) => Promise<AppUser> GetAppUser?: (req: GetAppUser_Input & {ctx: AppContext }) => Promise<AppUser>
GetAppUserLNURLInfo?: (req: GetAppUserLNURLInfo_Input & {ctx: AppContext }) => Promise<LnurlPayInfoResponse> GetAppUserLNURLInfo?: (req: GetAppUserLNURLInfo_Input & {ctx: AppContext }) => Promise<LnurlPayInfoResponse>
@ -2610,6 +2614,29 @@ export const ManageOperationValidate = (o?: ManageOperation, opts: ManageOperati
return null return null
} }
export type MessagingToken = {
device_id: string
firebase_messaging_token: string
}
export const MessagingTokenOptionalFields: [] = []
export type MessagingTokenOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
device_id_CustomCheck?: (v: string) => boolean
firebase_messaging_token_CustomCheck?: (v: string) => boolean
}
export const MessagingTokenValidate = (o?: MessagingToken, opts: MessagingTokenOptions = {}, path: string = 'MessagingToken::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if (typeof o.device_id !== 'string') return new Error(`${path}.device_id: is not a string`)
if (opts.device_id_CustomCheck && !opts.device_id_CustomCheck(o.device_id)) return new Error(`${path}.device_id: custom check failed`)
if (typeof o.firebase_messaging_token !== 'string') return new Error(`${path}.firebase_messaging_token: is not a string`)
if (opts.firebase_messaging_token_CustomCheck && !opts.firebase_messaging_token_CustomCheck(o.firebase_messaging_token)) return new Error(`${path}.firebase_messaging_token: custom check failed`)
return null
}
export type MetricsFile = { export type MetricsFile = {
} }
export const MetricsFileOptionalFields: [] = [] export const MetricsFileOptionalFields: [] = []

View file

@ -672,6 +672,12 @@ service LightningPub {
option (http_route) = "/api/user/http_creds"; option (http_route) = "/api/user/http_creds";
option (nostr) = true; option (nostr) = true;
} }
rpc EnrollMessagingToken(structs.MessagingToken) returns (structs.Empty){
option (auth_type) = "User";
option (http_method) = "post";
option (http_route) = "/api/user/messaging/enroll";
option (nostr) = true;
}
rpc BatchUser(structs.Empty) returns (structs.Empty){ rpc BatchUser(structs.Empty) returns (structs.Empty){
option (auth_type) = "User"; option (auth_type) = "User";
option (http_method) = "post"; option (http_method) = "post";

View file

@ -806,4 +806,7 @@ message ProvidersDisruption {
repeated ProviderDisruption disruptions = 1; repeated ProviderDisruption disruptions = 1;
} }
message MessagingToken {
string device_id = 1;
string firebase_messaging_token = 2;
}

View file

@ -0,0 +1,51 @@
// This file was autogenerated from a .proto file, DO NOT EDIT!
import axios from 'axios'
import * as Types from './types.js'
export type ResultError = { status: 'ERROR', reason: string }
export type ClientParams = {
baseUrl: string
retrieveAdminAuth: () => Promise<string | null>
retrieveGuestAuth: () => Promise<string | null>
retrieveNostrAppAuth: (rawBody: string, reqUrl: string, httpMethod: string) => Promise<string | null>
encryptCallback: (plain: any) => Promise<any>
decryptCallback: (encrypted: any) => Promise<any>
deviceId: string
checkResult?: true
}
export default (params: ClientParams) => ({
EnrollServicePub: async (request: Types.ServiceNpub): Promise<ResultError | ({ status: 'OK' })> => {
let finalRoute = '/api/admin/service/enroll'
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
const { data } = await axios.post(params.baseUrl + finalRoute, request, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
return data
}
return { status: 'ERROR', reason: 'invalid response' }
},
Health: async (): Promise<ResultError | ({ status: 'OK' })> => {
let finalRoute = '/api/health'
const auth = await params.retrieveGuestAuth()
if (auth === null) throw new Error('retrieveGuestAuth() returned null')
const { data } = await axios.get(params.baseUrl + finalRoute, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
return data
}
return { status: 'ERROR', reason: 'invalid response' }
},
SendNotification: async (request: Types.Notification): Promise<ResultError | ({ status: 'OK' })> => {
let finalRoute = '/api/user/notification'
const rawBody = JSON.stringify(request)
const auth = await params.retrieveNostrAppAuth(rawBody, finalRoute, 'post')
if (auth === null) throw new Error('retrieveNostrAppAuth() returned null')
const { data } = await axios.post(params.baseUrl + finalRoute, rawBody, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
return data
}
return { status: 'ERROR', reason: 'invalid response' }
},
})

View file

@ -0,0 +1,104 @@
// This file was autogenerated from a .proto file, DO NOT EDIT!
export type ResultError = { status: 'ERROR', reason: string }
export type RequestInfo = { rpcName: string, batch: boolean, nostr: boolean, batchSize: number }
export type RequestStats = { startMs: number, start: bigint, parse: bigint, guard: bigint, validate: bigint, handle: bigint }
export type RequestMetric = AuthContext & RequestInfo & RequestStats & { error?: string }
export type ProtoSocketState = 'OPEN' | 'CLOSED'
export type ProtoSocket<T> = {
getState: () => ProtoSocketState
send: (res: T, err: Error | null) => void
}
export type AdminContext = {
admin_id: string
}
export type AdminMethodInputs = EnrollServicePub_Input
export type AdminMethodOutputs = EnrollServicePub_Output
export type GuestContext = {
}
export type GuestMethodInputs = Health_Input
export type GuestMethodOutputs = Health_Output
export type NostrAppContext = {
nostr_app_npub: string
}
export type NostrAppMethodInputs = SendNotification_Input
export type NostrAppMethodOutputs = SendNotification_Output
export type AuthContext = AdminContext | GuestContext | NostrAppContext
export type EnrollServicePub_Input = { rpcName: 'EnrollServicePub', req: ServiceNpub }
export type EnrollServicePub_Output = ResultError | { status: 'OK' }
export type Health_Input = { rpcName: 'Health' }
export type Health_Output = ResultError | { status: 'OK' }
export type SendNotification_Input = { rpcName: 'SendNotification', req: Notification }
export type SendNotification_Output = ResultError | { status: 'OK' }
export type ServerMethods = {
EnrollServicePub?: (req: EnrollServicePub_Input & { ctx: AdminContext }) => Promise<void>
Health?: (req: Health_Input & { ctx: GuestContext }) => Promise<void>
SendNotification?: (req: SendNotification_Input & { ctx: NostrAppContext }) => Promise<void>
}
export type OptionsBaseMessage = {
allOptionalsAreSet?: true
}
export type Empty = {
}
export const EmptyOptionalFields: [] = []
export type EmptyOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
}
export const EmptyValidate = (o?: Empty, opts: EmptyOptions = {}, path: string = 'Empty::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
return null
}
export type Notification = {
data: string
recipient_registration_tokens: string[]
}
export const NotificationOptionalFields: [] = []
export type NotificationOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
data_CustomCheck?: (v: string) => boolean
recipient_registration_tokens_CustomCheck?: (v: string[]) => boolean
}
export const NotificationValidate = (o?: Notification, opts: NotificationOptions = {}, path: string = 'Notification::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if (typeof o.data !== 'string') return new Error(`${path}.data: is not a string`)
if (opts.data_CustomCheck && !opts.data_CustomCheck(o.data)) return new Error(`${path}.data: custom check failed`)
if (!Array.isArray(o.recipient_registration_tokens)) return new Error(`${path}.recipient_registration_tokens: is not an array`)
for (let index = 0; index < o.recipient_registration_tokens.length; index++) {
if (typeof o.recipient_registration_tokens[index] !== 'string') return new Error(`${path}.recipient_registration_tokens[${index}]: is not a string`)
}
if (opts.recipient_registration_tokens_CustomCheck && !opts.recipient_registration_tokens_CustomCheck(o.recipient_registration_tokens)) return new Error(`${path}.recipient_registration_tokens: custom check failed`)
return null
}
export type ServiceNpub = {
npub: string
}
export const ServiceNpubOptionalFields: [] = []
export type ServiceNpubOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
npub_CustomCheck?: (v: string) => boolean
}
export const ServiceNpubValidate = (o?: ServiceNpub, opts: ServiceNpubOptions = {}, path: string = 'ServiceNpub::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if (typeof o.npub !== 'string') return new Error(`${path}.npub: is not a string`)
if (opts.npub_CustomCheck && !opts.npub_CustomCheck(o.npub)) return new Error(`${path}.npub: custom check failed`)
return null
}

View file

@ -0,0 +1,61 @@
import { nip98, UnsignedEvent, finalizeEvent } from 'nostr-tools'
import { bytesToHex } from '@noble/hashes/utils'
import { sha256 } from '@noble/hashes/sha256'
import { base64 } from '@scure/base';
import NewClient, { ClientParams } from './autogenerated/http_client.js'
import { ERROR, getLogger } from '../helpers/logger.js'
const utf8Encoder = new TextEncoder()
export type PushPair = { pubkey: string, privateKey: string }
const nip98Kind = 27235
export class ShockPush {
private client: ReturnType<typeof NewClient>
private logger: ReturnType<typeof getLogger>
private serviceBaseUrl: string
private pair: PushPair
constructor(shockPushUrl: string, pair: PushPair) {
this.logger = getLogger({ component: 'shockPush' })
this.serviceBaseUrl = shockPushUrl
this.pair = pair
this.client = NewClient({
baseUrl: this.serviceBaseUrl,
retrieveAdminAuth: async () => { throw new Error('not implemented') },
retrieveGuestAuth: async () => (''),
retrieveNostrAppAuth: async (rawBody, reqUrl, httpMethod) => this.generateNip98Header(rawBody, reqUrl, httpMethod),
encryptCallback: () => { throw new Error('not implemented') },
decryptCallback: () => { throw new Error('not implemented') },
deviceId: '',
})
}
private generateNip98Header = async (raw: string, url: string, method: string) => {
const tags = [
["u", `${this.serviceBaseUrl}${url}`],
["method", method.toUpperCase()]
];
if (raw !== "") {
tags.push(["payload", bytesToHex(sha256(utf8Encoder.encode(raw)))]);
}
const npub = this.pair.pubkey
const event: UnsignedEvent = {
created_at: Math.round(Date.now() / 1000),
pubkey: npub,
content: "",
kind: nip98Kind,
tags
}
const signed = finalizeEvent(event, Buffer.from(this.pair.privateKey, 'hex'))
const nip98Header = "Nostr " + base64.encode(utf8Encoder.encode(JSON.stringify(signed)));
return nip98Header
}
SendNotification = async (message: string, messagingToken: string) => {
const res = await this.client.SendNotification({ recipient_registration_tokens: [messagingToken], data: message })
if (res.status !== 'OK') {
this.logger(ERROR, `failed to send notification: ${res.status}`)
}
}
}

View file

@ -111,4 +111,10 @@ export default class {
user_identifier: ctx.app_user_id user_identifier: ctx.app_user_id
}) })
} }
async EnrollMessagingToken(ctx: Types.UserContext, req: Types.MessagingToken): Promise<void> {
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id);
const user = await this.storage.applicationStorage.GetApplicationUser(app, ctx.app_user_id);
await this.storage.applicationStorage.UpdateAppUserMessagingToken(user.identifier, req.device_id, req.firebase_messaging_token);
}
} }

View file

@ -316,4 +316,6 @@ export default class {
await this.storage.applicationStorage.SetInviteTokenAsUsed(inviteToken); await this.storage.applicationStorage.SetInviteTokenAsUsed(inviteToken);
} }
} }

View file

@ -28,6 +28,7 @@ import { parse } from "uri-template"
import webRTC from "../webRTC/index.js" import webRTC from "../webRTC/index.js"
import { ManagementManager } from "./managementManager.js" import { ManagementManager } from "./managementManager.js"
import { Agent } from "https" import { Agent } from "https"
import { NotificationsManager } from "./notificationsManager.js"
type UserOperationsSub = { type UserOperationsSub = {
id: string id: string
@ -58,6 +59,7 @@ export default class {
utils: Utils utils: Utils
rugPullTracker: RugPullTracker rugPullTracker: RugPullTracker
unlocker: Unlocker unlocker: Unlocker
notificationsManager: NotificationsManager
//webRTC: webRTC //webRTC: webRTC
nostrSend: NostrSend = () => { getLogger({})("nostr send not initialized yet") } nostrSend: NostrSend = () => { getLogger({})("nostr send not initialized yet") }
nostrProcessPing: (() => Promise<void>) | null = null nostrProcessPing: (() => Promise<void>) | null = null
@ -81,6 +83,7 @@ export default class {
this.debitManager = new DebitManager(this.storage, this.lnd, this.applicationManager) this.debitManager = new DebitManager(this.storage, this.lnd, this.applicationManager)
this.offerManager = new OfferManager(this.storage, this.settings, this.lnd, this.applicationManager, this.productManager, this.liquidityManager) this.offerManager = new OfferManager(this.storage, this.settings, this.lnd, this.applicationManager, this.productManager, this.liquidityManager)
this.managementManager = new ManagementManager(this.storage, this.settings) this.managementManager = new ManagementManager(this.storage, this.settings)
this.notificationsManager = new NotificationsManager(this.settings.shockPushBaseUrl)
//this.webRTC = new webRTC(this.storage, this.utils) //this.webRTC = new webRTC(this.storage, this.utils)
} }
@ -287,7 +290,8 @@ export default class {
async triggerPaidCallback(log: PubLogger, url: string, async triggerPaidCallback(log: PubLogger, url: string,
{ invoice, amount, payerData, token, rejectUnauthorized }: { invoice, amount, payerData, token, rejectUnauthorized }:
{ invoice: string, {
invoice: string,
amount: number, amount: number,
payerData?: Record<string, string>, payerData?: Record<string, string>,
token?: string, token?: string,
@ -357,8 +361,16 @@ export default class {
getLogger({ appName: app.name })("cannot notify user, not a nostr user") getLogger({ appName: app.name })("cannot notify user, not a nostr user")
return return
} }
const devices = await this.storage.applicationStorage.GetAppUserDevices(user.identifier)
const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = { operation: op, requestId: "GetLiveUserOperations", status: 'OK' } const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = { operation: op, requestId: "GetLiveUserOperations", status: 'OK' }
this.nostrSend({ type: 'app', appId: app.app_id }, { type: 'content', content: JSON.stringify(message), pub: user.nostr_public_key }) const j = JSON.stringify(message)
this.nostrSend({ type: 'app', appId: app.app_id }, { type: 'content', content: j, pub: user.nostr_public_key })
for (const device of devices) {
this.notificationsManager.SendNotification(JSON.stringify(message), device.firebase_messaging_token, {
pubkey: app.nostr_public_key!,
privateKey: app.nostr_private_key!
})
}
} }
async UpdateBeacon(app: Application, content: { type: 'service', name: string }) { async UpdateBeacon(app: Application, content: { type: 'service', name: string }) {

View file

@ -0,0 +1,31 @@
import { PushPair, ShockPush } from "../ShockPush"
import { getLogger, PubLogger } from "../helpers/logger"
export class NotificationsManager {
private shockPushBaseUrl: string
private clients: Record<string, ShockPush> = {}
private logger: PubLogger
constructor(shockPushBaseUrl: string) {
this.shockPushBaseUrl = shockPushBaseUrl
this.logger = getLogger({ component: 'notificationsManager' })
}
private getClient = (pair: PushPair) => {
const client = this.clients[pair.pubkey]
if (client) {
return client
}
const newClient = new ShockPush(this.shockPushBaseUrl, pair)
this.clients[pair.pubkey] = newClient
return newClient
}
SendNotification = async (message: string, messagingToken: string, pair: PushPair) => {
if (!this.shockPushBaseUrl) {
this.logger("ShockPush is not configured, skipping notification")
return
}
const client = this.getClient(pair)
await client.SendNotification(message, messagingToken)
}
}

View file

@ -39,6 +39,7 @@ export type MainSettings = {
bridgeUrl: string, bridgeUrl: string,
allowResetMetricsStorages: boolean allowResetMetricsStorages: boolean
allowHttpUpgrade: boolean allowHttpUpgrade: boolean
shockPushBaseUrl: string
} }
export type BitcoinCoreSettings = { export type BitcoinCoreSettings = {
@ -81,7 +82,8 @@ export const LoadMainSettingsFromEnv = (): MainSettings => {
lnurlMetaText: process.env.LNURL_META_TEXT || "LNURL via Lightning.pub", lnurlMetaText: process.env.LNURL_META_TEXT || "LNURL via Lightning.pub",
bridgeUrl: process.env.BRIDGE_URL || "https://shockwallet.app", bridgeUrl: process.env.BRIDGE_URL || "https://shockwallet.app",
allowResetMetricsStorages: process.env.ALLOW_RESET_METRICS_STORAGES === 'true' || false, allowResetMetricsStorages: process.env.ALLOW_RESET_METRICS_STORAGES === 'true' || false,
allowHttpUpgrade: process.env.ALLOW_HTTP_UPGRADE === 'true' || false allowHttpUpgrade: process.env.ALLOW_HTTP_UPGRADE === 'true' || false,
shockPushBaseUrl: process.env.SHOCK_PUSH_URL || ""
} }
} }

View file

@ -426,5 +426,13 @@ export default (mainHandler: Main): Types.ServerMethods => {
GetHttpCreds: async ({ ctx }) => { GetHttpCreds: async ({ ctx }) => {
return mainHandler.appUserManager.GetHttpCreds(ctx) return mainHandler.appUserManager.GetHttpCreds(ctx)
}, },
EnrollMessagingToken: async ({ ctx, req }) => {
const err = Types.MessagingTokenValidate(req, {
device_id_CustomCheck: id => id !== '',
firebase_messaging_token_CustomCheck: token => token !== ''
})
if (err != null) throw new Error(err.message)
return mainHandler.appUserManager.EnrollMessagingToken(ctx, req)
},
} }
} }

View file

@ -8,6 +8,7 @@ import { getLogger } from '../helpers/logger.js';
import { User } from './entity/User.js'; import { User } from './entity/User.js';
import { InviteToken } from './entity/InviteToken.js'; import { InviteToken } from './entity/InviteToken.js';
import { StorageInterface } from './db/storageInterface.js'; import { StorageInterface } from './db/storageInterface.js';
import { AppUserDevice } from './entity/AppUserDevice.js';
export default class { export default class {
dbs: StorageInterface dbs: StorageInterface
userStorage: UserStorage userStorage: UserStorage
@ -178,4 +179,23 @@ export default class {
return this.dbs.Update<InviteToken>('InviteToken', inviteToken, { used: true }) return this.dbs.Update<InviteToken>('InviteToken', inviteToken, { used: true })
} }
async UpdateAppUserMessagingToken(appUserId: string, deviceId: string, firebaseMessagingToken: string) {
const existing = await this.dbs.FindOne<AppUserDevice>('AppUserDevice', { where: { app_user_id: appUserId, device_id: deviceId } })
if (!existing) {
return this.dbs.CreateAndSave<AppUserDevice>('AppUserDevice', {
app_user_id: appUserId,
device_id: deviceId,
firebase_messaging_token: firebaseMessagingToken
})
}
if (existing.firebase_messaging_token === firebaseMessagingToken) {
return
}
return this.dbs.Update<AppUserDevice>('AppUserDevice', existing.serial_id, { firebase_messaging_token: firebaseMessagingToken })
}
async GetAppUserDevices(appUserId: string, txId?: string) {
return this.dbs.Find<AppUserDevice>('AppUserDevice', { where: { app_user_id: appUserId } }, txId)
}
} }

View file

@ -26,6 +26,7 @@ import { RootOperation } from "../entity/RootOperation.js"
import { UserOffer } from "../entity/UserOffer.js" import { UserOffer } from "../entity/UserOffer.js"
import { ManagementGrant } from "../entity/ManagementGrant.js" import { ManagementGrant } from "../entity/ManagementGrant.js"
import { ChannelEvent } from "../entity/ChannelEvent.js" import { ChannelEvent } from "../entity/ChannelEvent.js"
import { AppUserDevice } from "../entity/AppUserDevice.js"
export type DbSettings = { export type DbSettings = {
@ -68,7 +69,8 @@ export const MainDbEntities = {
'DebitAccess': DebitAccess, 'DebitAccess': DebitAccess,
'UserOffer': UserOffer, 'UserOffer': UserOffer,
'Product': Product, 'Product': Product,
'ManagementGrant': ManagementGrant 'ManagementGrant': ManagementGrant,
'AppUserDevice': AppUserDevice
} }
export type MainDbNames = keyof typeof MainDbEntities export type MainDbNames = keyof typeof MainDbEntities
export const MainDbEntitiesNames = Object.keys(MainDbEntities) export const MainDbEntitiesNames = Object.keys(MainDbEntities)

View file

@ -0,0 +1,22 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from "typeorm";
@Entity()
export class AppUserDevice {
@PrimaryGeneratedColumn()
serial_id: number
@Column()
app_user_id: string
@Column()
device_id: string
@Column()
firebase_messaging_token: string
@CreateDateColumn()
created_at: Date
@UpdateDateColumn()
updated_at: Date
}

View file

@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AppUserDevice1753285173175 implements MigrationInterface {
name = 'AppUserDevice1753285173175'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "app_user_device" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "app_user_id" varchar NOT NULL, "device_id" varchar NOT NULL, "firebase_messaging_token" varchar NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')))`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "app_user_device"`);
}
}

View file

@ -20,9 +20,11 @@ import { ChannelEvents1750777346411 } from './1750777346411-channel_events.js'
import { ManagementGrant1751307732346 } from './1751307732346-management_grant.js' import { ManagementGrant1751307732346 } from './1751307732346-management_grant.js'
import { ManagementGrantBanned1751989251513 } from './1751989251513-management_grant_banned.js' import { ManagementGrantBanned1751989251513 } from './1751989251513-management_grant_banned.js'
import { InvoiceCallbackUrls1752425992291 } from './1752425992291-invoice_callback_urls.js' import { InvoiceCallbackUrls1752425992291 } from './1752425992291-invoice_callback_urls.js'
import { AppUserDevice1753285173175 } from './1753285173175-app_user_device.js'
export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189,
TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264,
DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513, InvoiceCallbackUrls1752425992291] DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513,
InvoiceCallbackUrls1752425992291, AppUserDevice1753285173175]
export const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825, RootOps1732566440447, RootOpsTime1745428134124, ChannelEvents1750777346411] export const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825, RootOps1732566440447, RootOpsTime1745428134124, ChannelEvents1750777346411]
/* export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise<boolean> => { /* export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise<boolean> => {
await connectAndMigrate(log, storageManager, allMigrations, allMetricsMigrations) await connectAndMigrate(log, storageManager, allMigrations, allMetricsMigrations)

View file

@ -1,5 +1,5 @@
import crypto from 'crypto'; import crypto from 'crypto';
import { Between, FindOperator, IsNull, LessThanOrEqual, MoreThan, MoreThanOrEqual, Not } from "typeorm" import { And, Between, Equal, FindOperator, IsNull, LessThanOrEqual, MoreThan, MoreThanOrEqual, Not } from "typeorm"
import { User } from './entity/User.js'; import { User } from './entity/User.js';
import { UserTransactionPayment } from './entity/UserTransactionPayment.js'; import { UserTransactionPayment } from './entity/UserTransactionPayment.js';
import { EphemeralKeyType, UserEphemeralKey } from './entity/UserEphemeralKey.js'; import { EphemeralKeyType, UserEphemeralKey } from './entity/UserEphemeralKey.js';
@ -73,6 +73,38 @@ export default class {
return this.dbs.Update<UserReceivingInvoice>('UserReceivingInvoice', invoice.serial_id, i, txId) return this.dbs.Update<UserReceivingInvoice>('UserReceivingInvoice', invoice.serial_id, i, txId)
} }
async GetUserInvoicesFlaggedAsPaid2(serialId: number, fromIndex: number, fromTs: number, take = 50, txId?: string): Promise<UserReceivingInvoice[]> {
let items = await this.dbs.Find<UserReceivingInvoice>('UserReceivingInvoice', {
where: {
user: { serial_id: serialId },
paid_at_unix: And(MoreThan(0), Equal(fromTs)),
serial_id: MoreThan(fromIndex)
},
order: {
paid_at_unix: 'DESC',
serial_id: 'DESC'
},
take
}, txId)
const more = take - items.length
if (more > 0) {
const more = await this.dbs.Find<UserReceivingInvoice>('UserReceivingInvoice', {
where: {
user: { serial_id: serialId },
paid_at_unix: And(MoreThan(0), MoreThan(fromTs)),
},
order: {
paid_at_unix: 'DESC',
serial_id: 'DESC'
},
take
}, txId)
items.push(...more)
}
return items
}
GetUserInvoicesFlaggedAsPaid(userId: string, fromIndex: number, take = 50, txId?: string): Promise<UserReceivingInvoice[]> { GetUserInvoicesFlaggedAsPaid(userId: string, fromIndex: number, take = 50, txId?: string): Promise<UserReceivingInvoice[]> {
return this.dbs.Find<UserReceivingInvoice>('UserReceivingInvoice', { return this.dbs.Find<UserReceivingInvoice>('UserReceivingInvoice', {
where: { where: {