store and use notifications token
This commit is contained in:
parent
5ee048a568
commit
c18f71c548
27 changed files with 564 additions and 28 deletions
|
|
@ -1,16 +1,16 @@
|
||||||
manifestVersion: 1
|
manifestVersion: 1
|
||||||
id: lightning-pub
|
id: lightning-pub
|
||||||
category: finance
|
category: finance
|
||||||
name: Lightning.Pub
|
name: Lightning.Pub
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
tagline: lightning, nostr, accounts, lnurl, web
|
tagline: lightning, nostr, accounts, lnurl, web
|
||||||
description: >-
|
description: >-
|
||||||
"Pub" is a Nostr-native account system designed
|
"Pub" is a Nostr-native account system designed
|
||||||
to make running Lightning infrastructure for your friends/family/customers
|
to make running Lightning infrastructure for your friends/family/customers
|
||||||
easier than previously thought possible.
|
easier than previously thought possible.
|
||||||
|
|
||||||
Being Nostr-native eliminates the complexity of configuring your node like a server by using commodity Nostr relays.
|
Being Nostr-native eliminates the complexity of configuring your node like a server by using commodity Nostr relays.
|
||||||
These relays, unlike LNURL proxies, are trustless by nature of Nostr's own encryption spec (NIP44).
|
These relays, unlike LNURL proxies, are trustless by nature of Nostr's own encryption spec (NIP44).
|
||||||
|
|
||||||
Support for optional services are integrated into Pub for operators seeking backward compatibility with legacy LNURLs and Lightning Addresses.
|
Support for optional services are integrated into Pub for operators seeking backward compatibility with legacy LNURLs and Lightning Addresses.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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: [] = []
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
51
src/services/ShockPush/autogenerated/http_client.ts
Normal file
51
src/services/ShockPush/autogenerated/http_client.ts
Normal 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' }
|
||||||
|
},
|
||||||
|
})
|
||||||
104
src/services/ShockPush/autogenerated/types.ts
Normal file
104
src/services/ShockPush/autogenerated/types.ts
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
61
src/services/ShockPush/index.ts
Normal file
61
src/services/ShockPush/index.ts
Normal 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}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -316,4 +316,6 @@ export default class {
|
||||||
await this.storage.applicationStorage.SetInviteTokenAsUsed(inviteToken);
|
await this.storage.applicationStorage.SetInviteTokenAsUsed(inviteToken);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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 }) {
|
||||||
|
|
|
||||||
31
src/services/main/notificationsManager.ts
Normal file
31
src/services/main/notificationsManager.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 || ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
22
src/services/storage/entity/AppUserDevice.ts
Normal file
22
src/services/storage/entity/AppUserDevice.ts
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
@ -14,7 +14,7 @@ import { Application } from './entity/Application.js';
|
||||||
import TransactionsQueue from "./db/transactionsQueue.js";
|
import TransactionsQueue from "./db/transactionsQueue.js";
|
||||||
import { LoggedEvent } from './eventsLog.js';
|
import { LoggedEvent } from './eventsLog.js';
|
||||||
import { StorageInterface } from './db/storageInterface.js';
|
import { StorageInterface } from './db/storageInterface.js';
|
||||||
export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo, offerId?: string, payerData?: Record<string, string> , rejectUnauthorized?: boolean, token?: string}
|
export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo, offerId?: string, payerData?: Record<string, string>, rejectUnauthorized?: boolean, token?: string }
|
||||||
export const defaultInvoiceExpiry = 60 * 60
|
export const defaultInvoiceExpiry = 60 * 60
|
||||||
export default class {
|
export default class {
|
||||||
dbs: StorageInterface
|
dbs: StorageInterface
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue