Pending Zaps
A fairly large change that replaces Zaps in the codebase with "Zapping" which is a tagged union consisting of a resolved Zap and a Pending Zap. These are both counted as Zaps everywhere in Damus, except pending zaps can be cancelled (most of the time).
This commit is contained in:
@@ -55,11 +55,42 @@ class PreviewModel: ObservableObject {
|
||||
}
|
||||
|
||||
class ZapsDataModel: ObservableObject {
|
||||
@Published var zaps: [Zap]
|
||||
@Published var zaps: [Zapping]
|
||||
|
||||
init(_ zaps: [Zap]) {
|
||||
init(_ zaps: [Zapping]) {
|
||||
self.zaps = zaps
|
||||
}
|
||||
|
||||
func update_state(reqid: String, state: PendingZapState) {
|
||||
guard let zap = zaps.first(where: { z in z.request.id == reqid }),
|
||||
case .pending(let pzap) = zap,
|
||||
pzap.state != state
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
pzap.state = state
|
||||
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
|
||||
var zap_total: Int64 {
|
||||
zaps.reduce(0) { total, zap in total + zap.amount }
|
||||
}
|
||||
|
||||
func from(_ pubkey: String) -> [Zapping] {
|
||||
return self.zaps.filter { z in z.request.pubkey == pubkey }
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func remove(reqid: String) -> Bool {
|
||||
guard zaps.first(where: { z in z.request.id == reqid }) != nil else {
|
||||
return false
|
||||
}
|
||||
|
||||
self.zaps = zaps.filter { z in z.request.id != reqid }
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class RelativeTimeModel: ObservableObject {
|
||||
@@ -86,7 +117,7 @@ class EventData {
|
||||
return preview_model.state
|
||||
}
|
||||
|
||||
init(zaps: [Zap] = []) {
|
||||
init(zaps: [Zapping] = []) {
|
||||
self.translations_model = .init(state: .havent_tried)
|
||||
self.artifacts_model = .init(state: .not_loaded)
|
||||
self.zaps_model = .init(zaps)
|
||||
@@ -131,12 +162,23 @@ class EventCache {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func store_zap(zap: Zap) -> Bool {
|
||||
func store_zap(zap: Zapping) -> Bool {
|
||||
let data = get_cache_data(zap.target.id).zaps_model
|
||||
return insert_uniq_sorted_zap_by_amount(zaps: &data.zaps, new_zap: zap)
|
||||
}
|
||||
|
||||
func lookup_zaps(target: ZapTarget) -> [Zap] {
|
||||
func remove_zap(zap: Zapping) {
|
||||
switch zap.target {
|
||||
case .note(let note_target):
|
||||
let zaps = get_cache_data(note_target.note_id).zaps_model
|
||||
zaps.remove(reqid: zap.request.id)
|
||||
case .profile:
|
||||
// these aren't stored anywhere yet
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func lookup_zaps(target: ZapTarget) -> [Zapping] {
|
||||
return get_cache_data(target.id).zaps_model.zaps
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,17 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap, cmp: (Zap, Zap) -> Bool) -> Bool {
|
||||
func insert_uniq_sorted_zap(zaps: inout [Zapping], new_zap: Zapping, cmp: (Zapping, Zapping) -> Bool) -> Bool {
|
||||
var i: Int = 0
|
||||
|
||||
for zap in zaps {
|
||||
// don't insert duplicate events
|
||||
if new_zap.event.id == zap.event.id {
|
||||
if new_zap.request.id == zap.request.id {
|
||||
// replace pending
|
||||
if !new_zap.is_pending && zap.is_pending {
|
||||
zaps[i] = new_zap
|
||||
return true
|
||||
}
|
||||
// don't insert duplicate events
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -28,16 +33,16 @@ func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap, cmp: (Zap, Zap) ->
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func insert_uniq_sorted_zap_by_created(zaps: inout [Zap], new_zap: Zap) -> Bool {
|
||||
func insert_uniq_sorted_zap_by_created(zaps: inout [Zapping], new_zap: Zapping) -> Bool {
|
||||
return insert_uniq_sorted_zap(zaps: &zaps, new_zap: new_zap) { (a, b) in
|
||||
a.event.created_at > b.event.created_at
|
||||
a.created_at > b.created_at
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func insert_uniq_sorted_zap_by_amount(zaps: inout [Zap], new_zap: Zap) -> Bool {
|
||||
func insert_uniq_sorted_zap_by_amount(zaps: inout [Zapping], new_zap: Zapping) -> Bool {
|
||||
return insert_uniq_sorted_zap(zaps: &zaps, new_zap: new_zap) { (a, b) in
|
||||
a.invoice.amount > b.invoice.amount
|
||||
a.amount > b.amount
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,16 +26,24 @@ class PostedEvent {
|
||||
let event: NostrEvent
|
||||
let skip_ephemeral: Bool
|
||||
var remaining: [Relayer]
|
||||
let flush_after: Date?
|
||||
|
||||
init(event: NostrEvent, remaining: [String], skip_ephemeral: Bool) {
|
||||
init(event: NostrEvent, remaining: [String], skip_ephemeral: Bool, flush_after: Date? = nil) {
|
||||
self.event = event
|
||||
self.skip_ephemeral = skip_ephemeral
|
||||
self.flush_after = flush_after
|
||||
self.remaining = remaining.map {
|
||||
Relayer(relay: $0, attempts: 0, retry_after: 2.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum CancelSendErr {
|
||||
case nothing_to_cancel
|
||||
case not_delayed
|
||||
case too_late
|
||||
}
|
||||
|
||||
class PostBox {
|
||||
let pool: RelayPool
|
||||
var events: [String: PostedEvent]
|
||||
@@ -46,12 +54,37 @@ class PostBox {
|
||||
pool.register_handler(sub_id: "postbox", handler: handle_event)
|
||||
}
|
||||
|
||||
// only works reliably on delay-sent events
|
||||
func cancel_send(evid: String) -> CancelSendErr? {
|
||||
guard let ev = events[evid] else {
|
||||
return .nothing_to_cancel
|
||||
}
|
||||
|
||||
guard let after = ev.flush_after else {
|
||||
return .not_delayed
|
||||
}
|
||||
|
||||
guard Date.now < after else {
|
||||
return .too_late
|
||||
}
|
||||
|
||||
events.removeValue(forKey: evid)
|
||||
return nil
|
||||
}
|
||||
|
||||
func try_flushing_events() {
|
||||
let now = Int64(Date().timeIntervalSince1970)
|
||||
for kv in events {
|
||||
let event = kv.value
|
||||
|
||||
// some are delayed
|
||||
if let after = event.flush_after, Date.now.timeIntervalSince1970 < after.timeIntervalSince1970 {
|
||||
continue
|
||||
}
|
||||
|
||||
for relayer in event.remaining {
|
||||
if relayer.last_attempt == nil || (now >= (relayer.last_attempt! + Int64(relayer.retry_after))) {
|
||||
if relayer.last_attempt == nil ||
|
||||
(now >= (relayer.last_attempt! + Int64(relayer.retry_after))) {
|
||||
print("attempt #\(relayer.attempts) to flush event '\(event.event.content)' to \(relayer.relay) after \(relayer.retry_after) seconds")
|
||||
flush_event(event, to_relay: relayer)
|
||||
}
|
||||
@@ -99,16 +132,20 @@ class PostBox {
|
||||
}
|
||||
}
|
||||
|
||||
func send(_ event: NostrEvent, to: [String]? = nil, skip_ephemeral: Bool = true) {
|
||||
func send(_ event: NostrEvent, to: [String]? = nil, skip_ephemeral: Bool = true, delay: TimeInterval? = nil) {
|
||||
// Don't add event if we already have it
|
||||
if events[event.id] != nil {
|
||||
return
|
||||
}
|
||||
|
||||
let remaining = to ?? pool.our_descriptors.map { $0.url.id }
|
||||
let posted_ev = PostedEvent(event: event, remaining: remaining, skip_ephemeral: skip_ephemeral)
|
||||
let after = delay.map { d in Date.now.addingTimeInterval(d) }
|
||||
let posted_ev = PostedEvent(event: event, remaining: remaining, skip_ephemeral: skip_ephemeral, flush_after: after)
|
||||
|
||||
events[event.id] = posted_ev
|
||||
|
||||
flush_event(posted_ev)
|
||||
if after == nil {
|
||||
flush_event(posted_ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,80 @@ struct WalletRequest<T: Codable>: Codable {
|
||||
let params: T?
|
||||
}
|
||||
|
||||
struct WalletResponseErr: Codable {
|
||||
let code: String?
|
||||
let message: String?
|
||||
}
|
||||
|
||||
struct PayInvoiceResponse: Decodable {
|
||||
let preimage: String
|
||||
}
|
||||
|
||||
enum WalletResponseResultType: String {
|
||||
case pay_invoice
|
||||
}
|
||||
|
||||
enum WalletResponseResult {
|
||||
case pay_invoice(PayInvoiceResponse)
|
||||
}
|
||||
|
||||
struct FullWalletResponse {
|
||||
let req_id: String
|
||||
let response: WalletResponse
|
||||
|
||||
init?(from: NostrEvent) async {
|
||||
guard let req_id = from.referenced_ids.first else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.req_id = req_id.ref_id
|
||||
|
||||
let ares = Task {
|
||||
guard let resp: WalletResponse = decode_json(from.content) else {
|
||||
let resp: WalletResponse? = nil
|
||||
return resp
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
guard let res = await ares.value else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.response = res
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct WalletResponse: Decodable {
|
||||
let result_type: WalletResponseResultType
|
||||
let error: WalletResponseErr?
|
||||
let result: WalletResponseResult
|
||||
|
||||
private enum CodingKeys: CodingKey {
|
||||
case result_type, error, result
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let result_type_str = try container.decode(String.self, forKey: .result_type)
|
||||
|
||||
guard let result_type = WalletResponseResultType(rawValue: result_type_str) else {
|
||||
throw DecodingError.typeMismatch(WalletResponseResultType.self, .init(codingPath: decoder.codingPath, debugDescription: "result_type \(result_type_str) is unknown"))
|
||||
}
|
||||
|
||||
self.result_type = result_type
|
||||
self.error = try container.decodeIfPresent(WalletResponseErr.self, forKey: .error)
|
||||
|
||||
switch result_type {
|
||||
case .pay_invoice:
|
||||
let res = try container.decode(PayInvoiceResponse.self, forKey: .result)
|
||||
self.result = .pay_invoice(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func make_wallet_pay_invoice_request(invoice: String) -> WalletRequest<PayInvoiceRequest> {
|
||||
let data = PayInvoiceRequest(invoice: invoice)
|
||||
return WalletRequest(method: "pay_invoice", params: data)
|
||||
@@ -92,12 +166,65 @@ func make_wallet_connect_request<T>(req: WalletRequest<T>, to_pk: String, keypai
|
||||
return create_encrypted_event(content, to_pk: to_pk, tags: tags, keypair: keypair, created_at: created_at, kind: 23194)
|
||||
}
|
||||
|
||||
func nwc_pay(url: WalletConnectURL, pool: RelayPool, post: PostBox, invoice: String) {
|
||||
func subscribe_to_nwc(url: WalletConnectURL, pool: RelayPool) {
|
||||
var filter: NostrFilter = .filter_kinds([NostrKind.nwc_response.rawValue])
|
||||
filter.authors = [url.pubkey]
|
||||
filter.limit = 0
|
||||
let sub = NostrSubscribe(filters: [filter], sub_id: "nwc")
|
||||
|
||||
pool.send(.subscribe(sub), to: [url.relay.id])
|
||||
}
|
||||
|
||||
func nwc_pay(url: WalletConnectURL, pool: RelayPool, post: PostBox, invoice: String) -> NostrEvent? {
|
||||
let req = make_wallet_pay_invoice_request(invoice: invoice)
|
||||
guard let ev = make_wallet_connect_request(req: req, to_pk: url.pubkey, keypair: url.keypair) else {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
try? pool.add_relay(url.relay, info: .ephemeral)
|
||||
post.send(ev, to: [url.relay.id], skip_ephemeral: false)
|
||||
try? pool.add_relay(.nwc(url: url.relay))
|
||||
subscribe_to_nwc(url: url, pool: pool)
|
||||
post.send(ev, to: [url.relay.id], skip_ephemeral: false, delay: 5.0)
|
||||
return ev
|
||||
}
|
||||
|
||||
|
||||
func nwc_success(zapcache: Zaps, resp: FullWalletResponse) {
|
||||
// find the pending zap and mark it as pending-confirmed
|
||||
for kv in zapcache.our_zaps {
|
||||
let zaps = kv.value
|
||||
|
||||
for zap in zaps {
|
||||
guard case .pending(let pzap) = zap,
|
||||
case .nwc(let nwc_state) = pzap.state,
|
||||
case .postbox_pending(let nwc_req) = nwc_state.state,
|
||||
nwc_req.id == resp.req_id
|
||||
else {
|
||||
continue
|
||||
}
|
||||
|
||||
nwc_state.state = .confirmed
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func nwc_error(zapcache: Zaps, evcache: EventCache, resp: FullWalletResponse) {
|
||||
// find a pending zap with the nwc request id associated with this response and remove it
|
||||
for kv in zapcache.our_zaps {
|
||||
let zaps = kv.value
|
||||
|
||||
for zap in zaps {
|
||||
guard case .pending(let pzap) = zap,
|
||||
case .nwc(let nwc_state) = pzap.state,
|
||||
case .postbox_pending(let req) = nwc_state.state,
|
||||
req.id == resp.req_id
|
||||
else {
|
||||
continue
|
||||
}
|
||||
|
||||
// remove the pending zap if there was an error
|
||||
remove_zap(reqid: pzap.request.ev.id, zapcache: zapcache, evcache: evcache)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct NoteZapTarget: Equatable {
|
||||
public struct NoteZapTarget: Equatable, Hashable {
|
||||
public let note_id: String
|
||||
public let author: String
|
||||
}
|
||||
@@ -41,6 +41,148 @@ public enum ZapTarget: Equatable {
|
||||
|
||||
struct ZapRequest {
|
||||
let ev: NostrEvent
|
||||
|
||||
}
|
||||
|
||||
enum ExtPendingZapStateType {
|
||||
case fetching_invoice
|
||||
case done
|
||||
}
|
||||
|
||||
class ExtPendingZapState: Equatable {
|
||||
static func == (lhs: ExtPendingZapState, rhs: ExtPendingZapState) -> Bool {
|
||||
return lhs.state == rhs.state
|
||||
}
|
||||
|
||||
var state: ExtPendingZapStateType
|
||||
|
||||
init(state: ExtPendingZapStateType) {
|
||||
self.state = state
|
||||
}
|
||||
}
|
||||
|
||||
enum PendingZapState: Equatable {
|
||||
case nwc(NWCPendingZapState)
|
||||
case external(ExtPendingZapState)
|
||||
}
|
||||
|
||||
|
||||
enum NWCStateType: Equatable {
|
||||
case fetching_invoice
|
||||
case cancel_fetching_invoice
|
||||
case postbox_pending(NostrEvent)
|
||||
case confirmed
|
||||
case failed
|
||||
}
|
||||
|
||||
class NWCPendingZapState: Equatable {
|
||||
var state: NWCStateType
|
||||
let url: WalletConnectURL
|
||||
|
||||
init(state: NWCStateType, url: WalletConnectURL) {
|
||||
self.state = state
|
||||
self.url = url
|
||||
}
|
||||
|
||||
static func == (lhs: NWCPendingZapState, rhs: NWCPendingZapState) -> Bool {
|
||||
return lhs.state == rhs.state && lhs.url == rhs.url
|
||||
}
|
||||
}
|
||||
|
||||
class PendingZap {
|
||||
let amount_msat: Int64
|
||||
let target: ZapTarget
|
||||
let request: ZapRequest
|
||||
let type: ZapType
|
||||
var state: PendingZapState
|
||||
|
||||
init(amount_msat: Int64, target: ZapTarget, request: ZapRequest, type: ZapType, state: PendingZapState) {
|
||||
self.amount_msat = amount_msat
|
||||
self.target = target
|
||||
self.request = request
|
||||
self.type = type
|
||||
self.state = state
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum Zapping {
|
||||
case zap(Zap)
|
||||
case pending(PendingZap)
|
||||
|
||||
var is_pending: Bool {
|
||||
switch self {
|
||||
case .zap:
|
||||
return false
|
||||
case .pending:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
var is_private: Bool {
|
||||
switch self {
|
||||
case .zap(let zap):
|
||||
return zap.private_request != nil
|
||||
case .pending(let pzap):
|
||||
return pzap.type == .priv
|
||||
}
|
||||
}
|
||||
|
||||
var amount: Int64 {
|
||||
switch self {
|
||||
case .zap(let zap):
|
||||
return zap.invoice.amount
|
||||
case .pending(let pzap):
|
||||
return pzap.amount_msat
|
||||
}
|
||||
}
|
||||
|
||||
var target: ZapTarget {
|
||||
switch self {
|
||||
case .zap(let zap):
|
||||
return zap.target
|
||||
case .pending(let pzap):
|
||||
return pzap.target
|
||||
}
|
||||
}
|
||||
|
||||
var request: NostrEvent {
|
||||
switch self {
|
||||
case .zap(let zap):
|
||||
return zap.request_ev
|
||||
case .pending(let pzap):
|
||||
return pzap.request.ev
|
||||
}
|
||||
}
|
||||
|
||||
var created_at: Int64 {
|
||||
switch self {
|
||||
case .zap(let zap):
|
||||
return zap.event.created_at
|
||||
case .pending(let pzap):
|
||||
// pending zaps are created right away
|
||||
return pzap.request.ev.created_at
|
||||
}
|
||||
}
|
||||
|
||||
var event: NostrEvent? {
|
||||
switch self {
|
||||
case .zap(let zap):
|
||||
return zap.event
|
||||
case .pending:
|
||||
// pending zaps don't have a zap event
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var is_anon: Bool {
|
||||
switch self {
|
||||
case .zap(let zap):
|
||||
return zap.is_anon
|
||||
case .pending(let pzap):
|
||||
return pzap.type == .anon
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Zap {
|
||||
@@ -246,7 +388,7 @@ func fetch_static_payreq(_ lnurl: String) async -> LNUrlPayRequest? {
|
||||
return endpoint
|
||||
}
|
||||
|
||||
func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent?, sats: Int, zap_type: ZapType, comment: String?) async -> String? {
|
||||
func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, sats: Int, zap_type: ZapType, comment: String?) async -> String? {
|
||||
guard var base_url = payreq.callback.flatMap({ URLComponents(string: $0) }) else {
|
||||
return nil
|
||||
}
|
||||
@@ -256,7 +398,7 @@ func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent?, sats: Int
|
||||
|
||||
var query = [URLQueryItem(name: "amount", value: "\(amount)")]
|
||||
|
||||
if let zapreq, zappable && zap_type != .non_zap, let json = encode_json(zapreq) {
|
||||
if zappable && zap_type != .non_zap, let json = encode_json(zapreq) {
|
||||
print("zapreq json: \(json)")
|
||||
query.append(URLQueryItem(name: "nostr", value: json))
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
import Foundation
|
||||
|
||||
class Zaps {
|
||||
var zaps: [String: Zap]
|
||||
var zaps: [String: Zapping]
|
||||
let our_pubkey: String
|
||||
var our_zaps: [String: [Zap]]
|
||||
var our_zaps: [String: [Zapping]]
|
||||
|
||||
var event_counts: [String: Int]
|
||||
var event_totals: [String: Int64]
|
||||
@@ -22,15 +22,42 @@ class Zaps {
|
||||
self.event_counts = [:]
|
||||
self.event_totals = [:]
|
||||
}
|
||||
|
||||
func remove_zap(reqid: String) -> Zapping? {
|
||||
var res: Zapping? = nil
|
||||
for kv in our_zaps {
|
||||
let ours = kv.value
|
||||
guard let zap = ours.first(where: { z in z.request.id == reqid }) else {
|
||||
continue
|
||||
}
|
||||
|
||||
res = zap
|
||||
|
||||
our_zaps[kv.key] = ours.filter { z in z.request.id != reqid }
|
||||
|
||||
if let count = event_counts[zap.target.id] {
|
||||
event_counts[zap.target.id] = count - 1
|
||||
}
|
||||
if let total = event_totals[zap.target.id] {
|
||||
event_totals[zap.target.id] = total - zap.amount
|
||||
}
|
||||
|
||||
// we found the request id, we can stop looking
|
||||
break
|
||||
}
|
||||
|
||||
self.zaps.removeValue(forKey: reqid)
|
||||
return res
|
||||
}
|
||||
|
||||
func add_zap(zap: Zap) {
|
||||
if zaps[zap.event.id] != nil {
|
||||
func add_zap(zap: Zapping) {
|
||||
if zaps[zap.request.id] != nil {
|
||||
return
|
||||
}
|
||||
self.zaps[zap.event.id] = zap
|
||||
self.zaps[zap.request.id] = zap
|
||||
|
||||
// record our zaps for an event
|
||||
if zap.request.ev.pubkey == our_pubkey {
|
||||
if zap.request.pubkey == our_pubkey {
|
||||
switch zap.target {
|
||||
case .note(let note_target):
|
||||
if our_zaps[note_target.note_id] == nil {
|
||||
@@ -44,7 +71,7 @@ class Zaps {
|
||||
}
|
||||
|
||||
// don't count tips to self. lame.
|
||||
guard zap.request.ev.pubkey != zap.target.pubkey else {
|
||||
guard zap.request.pubkey != zap.target.pubkey else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -58,8 +85,15 @@ class Zaps {
|
||||
}
|
||||
|
||||
event_counts[id] = event_counts[id]! + 1
|
||||
event_totals[id] = event_totals[id]! + zap.invoice.amount
|
||||
event_totals[id] = event_totals[id]! + zap.amount
|
||||
|
||||
notify(.update_stats, zap.target.id)
|
||||
}
|
||||
}
|
||||
|
||||
func remove_zap(reqid: String, zapcache: Zaps, evcache: EventCache) {
|
||||
guard let zap = zapcache.remove_zap(reqid: reqid) else {
|
||||
return
|
||||
}
|
||||
evcache.get_cache_data(zap.target.id).zaps_model.remove(reqid: reqid)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user