mirror of
https://github.com/aljazceru/njump.git
synced 2025-12-17 14:24:27 +01:00
commands to ban (and unban) events using the relay management api.
This commit is contained in:
109
internal.pb.go
109
internal.pb.go
@@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.34.2
|
// protoc-gen-go v1.34.2
|
||||||
// protoc v5.28.1
|
// protoc v5.28.2
|
||||||
// source: internal.proto
|
// source: internal.proto
|
||||||
|
|
||||||
package main
|
package main
|
||||||
@@ -185,7 +185,7 @@ func (x *PubKeyArchive) GetPubkey() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
type EventInRelay struct {
|
type ID struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
@@ -193,8 +193,8 @@ type EventInRelay struct {
|
|||||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *EventInRelay) Reset() {
|
func (x *ID) Reset() {
|
||||||
*x = EventInRelay{}
|
*x = ID{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_internal_proto_msgTypes[3]
|
mi := &file_internal_proto_msgTypes[3]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
@@ -202,13 +202,13 @@ func (x *EventInRelay) Reset() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *EventInRelay) String() string {
|
func (x *ID) String() string {
|
||||||
return protoimpl.X.MessageStringOf(x)
|
return protoimpl.X.MessageStringOf(x)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*EventInRelay) ProtoMessage() {}
|
func (*ID) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *EventInRelay) ProtoReflect() protoreflect.Message {
|
func (x *ID) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_internal_proto_msgTypes[3]
|
mi := &file_internal_proto_msgTypes[3]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
@@ -220,18 +220,73 @@ func (x *EventInRelay) ProtoReflect() protoreflect.Message {
|
|||||||
return mi.MessageOf(x)
|
return mi.MessageOf(x)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deprecated: Use EventInRelay.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ID.ProtoReflect.Descriptor instead.
|
||||||
func (*EventInRelay) Descriptor() ([]byte, []int) {
|
func (*ID) Descriptor() ([]byte, []int) {
|
||||||
return file_internal_proto_rawDescGZIP(), []int{3}
|
return file_internal_proto_rawDescGZIP(), []int{3}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *EventInRelay) GetId() string {
|
func (x *ID) GetId() string {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.Id
|
return x.Id
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BannedEvent struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Id []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||||
|
Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *BannedEvent) Reset() {
|
||||||
|
*x = BannedEvent{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_proto_msgTypes[4]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *BannedEvent) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*BannedEvent) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *BannedEvent) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_proto_msgTypes[4]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use BannedEvent.ProtoReflect.Descriptor instead.
|
||||||
|
func (*BannedEvent) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_proto_rawDescGZIP(), []int{4}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *BannedEvent) GetId() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *BannedEvent) GetReason() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Reason
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
var File_internal_proto protoreflect.FileDescriptor
|
var File_internal_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
var file_internal_proto_rawDesc = []byte{
|
var file_internal_proto_rawDesc = []byte{
|
||||||
@@ -248,11 +303,14 @@ var file_internal_proto_rawDesc = []byte{
|
|||||||
0x75, 0x62, 0x6b, 0x65, 0x79, 0x73, 0x22, 0x27, 0x0a, 0x0d, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79,
|
0x75, 0x62, 0x6b, 0x65, 0x79, 0x73, 0x22, 0x27, 0x0a, 0x0d, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79,
|
||||||
0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65,
|
0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65,
|
||||||
0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x22,
|
0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x22,
|
||||||
0x1e, 0x0a, 0x0c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x12,
|
0x14, 0x0a, 0x02, 0x49, 0x44, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||||
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x42,
|
0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x35, 0x0a, 0x0b, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x45,
|
||||||
0x1f, 0x5a, 0x1d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x69,
|
0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c,
|
||||||
0x61, 0x74, 0x6a, 0x61, 0x66, 0x2f, 0x6e, 0x6a, 0x75, 0x6d, 0x70, 0x3b, 0x6d, 0x61, 0x69, 0x6e,
|
0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02,
|
||||||
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x42, 0x1f, 0x5a, 0x1d,
|
||||||
|
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x69, 0x61, 0x74, 0x6a,
|
||||||
|
0x61, 0x66, 0x2f, 0x6e, 0x6a, 0x75, 0x6d, 0x70, 0x3b, 0x6d, 0x61, 0x69, 0x6e, 0x62, 0x06, 0x70,
|
||||||
|
0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -267,12 +325,13 @@ func file_internal_proto_rawDescGZIP() []byte {
|
|||||||
return file_internal_proto_rawDescData
|
return file_internal_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_internal_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
|
var file_internal_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
|
||||||
var file_internal_proto_goTypes = []any{
|
var file_internal_proto_goTypes = []any{
|
||||||
(*CachedEvent)(nil), // 0: CachedEvent
|
(*CachedEvent)(nil), // 0: CachedEvent
|
||||||
(*FollowListArchive)(nil), // 1: FollowListArchive
|
(*FollowListArchive)(nil), // 1: FollowListArchive
|
||||||
(*PubKeyArchive)(nil), // 2: PubKeyArchive
|
(*PubKeyArchive)(nil), // 2: PubKeyArchive
|
||||||
(*EventInRelay)(nil), // 3: EventInRelay
|
(*ID)(nil), // 3: ID
|
||||||
|
(*BannedEvent)(nil), // 4: BannedEvent
|
||||||
}
|
}
|
||||||
var file_internal_proto_depIdxs = []int32{
|
var file_internal_proto_depIdxs = []int32{
|
||||||
0, // [0:0] is the sub-list for method output_type
|
0, // [0:0] is the sub-list for method output_type
|
||||||
@@ -325,7 +384,19 @@ func file_internal_proto_init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_internal_proto_msgTypes[3].Exporter = func(v any, i int) any {
|
file_internal_proto_msgTypes[3].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*EventInRelay); i {
|
switch v := v.(*ID); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_proto_msgTypes[4].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*BannedEvent); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
case 1:
|
case 1:
|
||||||
@@ -343,7 +414,7 @@ func file_internal_proto_init() {
|
|||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: file_internal_proto_rawDesc,
|
RawDescriptor: file_internal_proto_rawDesc,
|
||||||
NumEnums: 0,
|
NumEnums: 0,
|
||||||
NumMessages: 4,
|
NumMessages: 5,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 0,
|
NumServices: 0,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ message PubKeyArchive {
|
|||||||
string pubkey = 1;
|
string pubkey = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message EventInRelay {
|
message ID {
|
||||||
string id = 1;
|
string id = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message BannedEvent {
|
||||||
|
bytes id = 1;
|
||||||
|
string reason = 2;
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const (
|
|||||||
TypeFollowListArchive leafdb.DataType = 3
|
TypeFollowListArchive leafdb.DataType = 3
|
||||||
TypePubKeyArchive leafdb.DataType = 4
|
TypePubKeyArchive leafdb.DataType = 4
|
||||||
TypeEventInRelay leafdb.DataType = 5
|
TypeEventInRelay leafdb.DataType = 5
|
||||||
|
TypeBannedEvent leafdb.DataType = 6
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewInternalDB(path string) (*InternalDB, error) {
|
func NewInternalDB(path string) (*InternalDB, error) {
|
||||||
@@ -36,7 +37,9 @@ func NewInternalDB(path string) (*InternalDB, error) {
|
|||||||
case TypePubKeyArchive:
|
case TypePubKeyArchive:
|
||||||
v = &PubKeyArchive{}
|
v = &PubKeyArchive{}
|
||||||
case TypeEventInRelay:
|
case TypeEventInRelay:
|
||||||
v = &EventInRelay{}
|
v = &ID{}
|
||||||
|
case TypeBannedEvent:
|
||||||
|
v = &BannedEvent{}
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("what is this? %v", t)
|
return nil, fmt.Errorf("what is this? %v", t)
|
||||||
}
|
}
|
||||||
@@ -74,6 +77,14 @@ func NewInternalDB(path string) (*InternalDB, error) {
|
|||||||
emit(pkb)
|
emit(pkb)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"banned-event": {
|
||||||
|
Version: 1,
|
||||||
|
Types: []leafdb.DataType{TypeBannedEvent},
|
||||||
|
Emit: func(t leafdb.DataType, value proto.Message, emit func([]byte)) {
|
||||||
|
ban := value.(*BannedEvent)
|
||||||
|
emit(ban.Id[0:8])
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Views: map[string]leafdb.ViewDefinition[proto.Message]{
|
Views: map[string]leafdb.ViewDefinition[proto.Message]{
|
||||||
"pubkey-archive": {
|
"pubkey-archive": {
|
||||||
@@ -92,7 +103,7 @@ func NewInternalDB(path string) (*InternalDB, error) {
|
|||||||
Emit: func(t leafdb.DataType, value proto.Message, emit func(idxkey []byte, t leafdb.DataType, value proto.Message)) {
|
Emit: func(t leafdb.DataType, value proto.Message, emit func(idxkey []byte, t leafdb.DataType, value proto.Message)) {
|
||||||
ee := value.(*CachedEvent)
|
ee := value.(*CachedEvent)
|
||||||
for _, r := range ee.Relays {
|
for _, r := range ee.Relays {
|
||||||
emit([]byte(trimProtocolAndEndingSlash(r)), TypeEventInRelay, &EventInRelay{Id: ee.Id})
|
emit([]byte(trimProtocolAndEndingSlash(r)), TypeEventInRelay, &ID{Id: ee.Id})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -194,10 +205,46 @@ func (internal *InternalDB) getRelaysForEvent(eventId string) []string {
|
|||||||
func (internal *InternalDB) getEventsInRelay(hostname string) iter.Seq[string] {
|
func (internal *InternalDB) getEventsInRelay(hostname string) iter.Seq[string] {
|
||||||
return func(yield func(string) bool) {
|
return func(yield func(string) bool) {
|
||||||
for value := range internal.DB.View(leafdb.ExactQuery("events-in-relay", []byte(hostname))) {
|
for value := range internal.DB.View(leafdb.ExactQuery("events-in-relay", []byte(hostname))) {
|
||||||
evtid := value.(*EventInRelay)
|
evtid := value.(*ID)
|
||||||
if !yield(evtid.Id) {
|
if !yield(evtid.Id) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (internal *InternalDB) banEvent(id, reason string) error {
|
||||||
|
idb, err := hex.DecodeString(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = internal.DB.AddOrReplace("banned-event", TypeBannedEvent, &BannedEvent{
|
||||||
|
Id: idb,
|
||||||
|
Reason: reason,
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (internal *InternalDB) unbanEvent(id string) error {
|
||||||
|
idb, err := hex.DecodeString(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = internal.DB.DeleteQuery(leafdb.ExactQuery("banned-event", idb[0:8]))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (internal *InternalDB) isBanned(id string) (bool, string) {
|
||||||
|
idb, err := hex.DecodeString(id)
|
||||||
|
if err != nil {
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for record := range internal.DB.Query(leafdb.ExactQuery("banned-event", idb[0:8])) {
|
||||||
|
return true, record.(*BannedEvent).Reason
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|||||||
3
main.go
3
main.go
@@ -129,6 +129,9 @@ func main() {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// admin
|
||||||
|
setupRelayManagement(relay)
|
||||||
|
|
||||||
// routes
|
// routes
|
||||||
mux := relay.Router()
|
mux := relay.Router()
|
||||||
mux.Handle("/njump/static/", http.StripPrefix("/njump/", http.FileServer(http.FS(static))))
|
mux.Handle("/njump/static/", http.StripPrefix("/njump/", http.FileServer(http.FS(static))))
|
||||||
|
|||||||
45
management.go
Normal file
45
management.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/fiatjaf/khatru"
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip86"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupRelayManagement(relay *khatru.Relay) {
|
||||||
|
relay.ManagementAPI.RejectAPICall = append(relay.ManagementAPI.RejectAPICall,
|
||||||
|
func(ctx context.Context, mp nip86.MethodParams) (reject bool, msg string) {
|
||||||
|
if slices.Contains(s.TrustedPubKeys, khatru.GetAuthed(ctx)) {
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
return true, "you are not a trusted pubkey"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
relay.ManagementAPI.BanEvent = func(ctx context.Context, id, reason string) error {
|
||||||
|
log.Info().Str("id", id).Str("reason", reason).Msg("banning event")
|
||||||
|
ch, err := sys.Store.QueryEvents(ctx, nostr.Filter{IDs: []string{id}})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for evt := range ch {
|
||||||
|
sys.Store.DeleteEvent(ctx, evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := internal.banEvent(id, reason); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
relay.ManagementAPI.AllowEvent = func(ctx context.Context, id, reason string) error {
|
||||||
|
log.Info().Str("id", id).Str("reason", reason).Msg("unbanning event")
|
||||||
|
if err := internal.unbanEvent(id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,6 +72,14 @@ func renderEvent(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if banned, reason := internal.isBanned(data.event.ID); banned {
|
||||||
|
w.Header().Set("Cache-Control", "max-age=60")
|
||||||
|
log.Warn().Err(err).Str("code", code).Str("reason", reason).Msg("event banned")
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
errorTemplate(ErrorPageParams{Errors: "event banned"}).Render(ctx, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// if we originally got a note code or an nevent with no hints
|
// if we originally got a note code or an nevent with no hints
|
||||||
// augment the URL to point to an nevent with hints -- redirect
|
// augment the URL to point to an nevent with hints -- redirect
|
||||||
if p, ok := decoded.(nostr.EventPointer); (ok && p.Author == "" && len(p.Relays) == 0) || prefix == "note" {
|
if p, ok := decoded.(nostr.EventPointer); (ok && p.Author == "" && len(p.Relays) == 0) || prefix == "note" {
|
||||||
|
|||||||
Reference in New Issue
Block a user