Compare commits
84 Commits
feature/ld
...
32dff9c67f
| Author | SHA1 | Date | |
|---|---|---|---|
| 32dff9c67f | |||
|
126b8b20e0
|
|||
|
5abf69f356
|
|||
|
210a69bd9b
|
|||
|
bbed3cd367
|
|||
|
7943da0f17
|
|||
| 620167eedf | |||
|
e077debfc2
|
|||
|
531b2c3002
|
|||
|
6d2bc729b8
|
|||
|
2630ec2af4
|
|||
| daed5c1eea | |||
| 2e9429bb32 | |||
|
37c15c7a62
|
|||
|
01ecea74ff
|
|||
|
f401a03590
|
|||
|
fff6dea100
|
|||
|
48ab96dda9
|
|||
|
7ac3130c18
|
|||
|
cbfa148051
|
|||
|
87d900b627
|
|||
|
926dc06294
|
|||
|
00b73b06d7
|
|||
|
0daac33915
|
|||
|
0e472bc311
|
|||
| 40b34d0935 | |||
|
61cb8f4941
|
|||
|
433ac4dc8e
|
|||
|
62fe0d8fac
|
|||
|
2a675fd135
|
|||
|
c2c3ebc2e1
|
|||
|
5a5c316c14
|
|||
| f0d5457ec1 | |||
|
5588e3b3e8
|
|||
|
8949d76d26
|
|||
| 8bc9bbdc33 | |||
| d6d09b57b8 | |||
|
1685d6ecf8
|
|||
|
5348a229a6
|
|||
|
bad3b7a2be
|
|||
|
b541e95bb7
|
|||
|
3f43fe8101
|
|||
|
231dfc8404
|
|||
|
eeb9b0a331
|
|||
|
08e783d185
|
|||
|
fa5dc8ca46
|
|||
|
bc34e9c5e0
|
|||
| f388bd0237 | |||
|
48041630ca
|
|||
|
2d1ff29eca
|
|||
| 46fa42e387 | |||
|
c6c5d80fb4
|
|||
|
c0f4e7925e
|
|||
|
49d24990b4
|
|||
|
619bd954b7
|
|||
|
e27c64b5f1
|
|||
|
b36baf26eb
|
|||
|
adedaa5f7b
|
|||
|
596ed7fccc
|
|||
|
5685e1b7bc
|
|||
|
c3b82fc2a9
|
|||
|
77e2fe5792
|
|||
|
bc43082839
|
|||
|
b09225543b
|
|||
|
f2507409a3
|
|||
|
46b4723999
|
|||
|
3f90a011c4
|
|||
|
3ba333e802
|
|||
| d9dff3e872 | |||
| 6ddeacb779 | |||
|
78aff3d796
|
|||
|
8f600f44bd
|
|||
|
819ecf6ad8
|
|||
|
945eaba5e1
|
|||
|
22d362e1a0
|
|||
|
d4e67a830c
|
|||
|
670b2da1ef
|
|||
|
ed5c5b3081
|
|||
| 4ee6bfddfa | |||
|
8b60890061
|
|||
|
0367450c4b
|
|||
|
e6f5623c7f
|
|||
| 367f566ccb | |||
|
80e69df75c
|
@@ -29,6 +29,7 @@
|
|||||||
|
|
||||||
#
|
#
|
||||||
# Service Integrations
|
# Service Integrations
|
||||||
|
# (sorted alphabetically by service name)
|
||||||
#
|
#
|
||||||
|
|
||||||
# BTCPAY_PUBLIC_URL='https://btcpay.example.com'
|
# BTCPAY_PUBLIC_URL='https://btcpay.example.com'
|
||||||
@@ -62,5 +63,9 @@
|
|||||||
|
|
||||||
# MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
|
# MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
|
||||||
|
|
||||||
|
# NOSTR_PRIVATE_KEY='123456abcdef...'
|
||||||
|
# NOSTR_PUBLIC_KEY='123456abcdef...'
|
||||||
|
# NOSTR_RELAY_URL='wss://nostr.kosmos.org'
|
||||||
|
|
||||||
# RS_STORAGE_URL='https://storage.kosmos.org'
|
# RS_STORAGE_URL='https://storage.kosmos.org'
|
||||||
# RS_REDIS_URL='redis://localhost:6379/2'
|
# RS_REDIS_URL='redis://localhost:6379/2'
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
PRIMARY_DOMAIN=kosmos.org
|
PRIMARY_DOMAIN=kosmos.org
|
||||||
|
AKKOUNTS_DOMAIN=accounts.kosmos.org
|
||||||
|
|
||||||
REDIS_URL='redis://localhost:6379/0'
|
REDIS_URL='redis://localhost:6379/0'
|
||||||
|
|
||||||
@@ -11,10 +12,15 @@ DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
|
|||||||
|
|
||||||
EJABBERD_API_URL='http://xmpp.example.com/api'
|
EJABBERD_API_URL='http://xmpp.example.com/api'
|
||||||
|
|
||||||
|
MASTODON_PUBLIC_URL='http://example.social'
|
||||||
|
|
||||||
LNDHUB_API_URL='http://localhost:3026'
|
LNDHUB_API_URL='http://localhost:3026'
|
||||||
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
|
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
|
||||||
LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
|
LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
|
||||||
|
|
||||||
|
NOSTR_PRIVATE_KEY='7c3ef7e448505f0615137af38569d01807d3b05b5005d5ecf8aaafcd40323cea'
|
||||||
|
NOSTR_PUBLIC_KEY='bdd76ce2934b2f591f9fad2ebe9da18f20d2921de527494ba00eeaa0a0efadcf'
|
||||||
|
|
||||||
RS_STORAGE_URL='https://storage.kosmos.org'
|
RS_STORAGE_URL='https://storage.kosmos.org'
|
||||||
RS_REDIS_URL='redis://localhost:6379/1'
|
RS_REDIS_URL='redis://localhost:6379/1'
|
||||||
|
|
||||||
|
|||||||
2
Gemfile
2
Gemfile
@@ -62,7 +62,7 @@ gem "sentry-rails"
|
|||||||
gem 'discourse_api'
|
gem 'discourse_api'
|
||||||
gem "lnurl"
|
gem "lnurl"
|
||||||
gem 'manifique'
|
gem 'manifique'
|
||||||
gem 'nostr'
|
gem 'nostr', '~> 0.6.0'
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
# Use sqlite3 as the database for Active Record
|
# Use sqlite3 as the database for Active Record
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ GEM
|
|||||||
ruby2_keywords
|
ruby2_keywords
|
||||||
e2mmap (0.1.0)
|
e2mmap (0.1.0)
|
||||||
ecdsa (1.2.0)
|
ecdsa (1.2.0)
|
||||||
ecdsa_ext (0.5.0)
|
ecdsa_ext (0.5.1)
|
||||||
ecdsa (~> 1.2.0)
|
ecdsa (~> 1.2.0)
|
||||||
erubi (1.12.0)
|
erubi (1.12.0)
|
||||||
et-orbi (1.2.7)
|
et-orbi (1.2.7)
|
||||||
@@ -278,9 +278,9 @@ GEM
|
|||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.16.0-x86_64-linux)
|
nokogiri (1.16.0-x86_64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nostr (0.5.0)
|
nostr (0.6.0)
|
||||||
bech32 (~> 1.4)
|
bech32 (~> 1.4)
|
||||||
bip-schnorr (~> 0.6)
|
bip-schnorr (~> 0.7)
|
||||||
ecdsa (~> 1.2)
|
ecdsa (~> 1.2)
|
||||||
event_emitter (~> 0.2)
|
event_emitter (~> 0.2)
|
||||||
faye-websocket (~> 0.11)
|
faye-websocket (~> 0.11)
|
||||||
@@ -517,7 +517,7 @@ DEPENDENCIES
|
|||||||
lockbox
|
lockbox
|
||||||
manifique
|
manifique
|
||||||
net-ldap
|
net-ldap
|
||||||
nostr
|
nostr (~> 0.6.0)
|
||||||
pagy (~> 6.0, >= 6.0.2)
|
pagy (~> 6.0, >= 6.0.2)
|
||||||
pg (~> 1.5)
|
pg (~> 1.5)
|
||||||
puma (~> 4.1)
|
puma (~> 4.1)
|
||||||
|
|||||||
@@ -42,6 +42,11 @@
|
|||||||
focus:ring-red-500 focus:ring-opacity-75;
|
focus:ring-red-500 focus:ring-opacity-75;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-outline-purple {
|
||||||
|
@apply border-2 border-purple-500 hover:bg-purple-100
|
||||||
|
focus:ring-purple-400 focus:ring-opacity-75;
|
||||||
|
}
|
||||||
|
|
||||||
.btn:disabled {
|
.btn:disabled {
|
||||||
@apply bg-gray-100 hover:bg-gray-200 text-gray-400
|
@apply bg-gray-100 hover:bg-gray-200 text-gray-400
|
||||||
focus:ring-gray-300 focus:ring-opacity-75;
|
focus:ring-gray-300 focus:ring-opacity-75;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@layer components {
|
@layer components {
|
||||||
.services > div > a {
|
.services > div > a {
|
||||||
background-image: linear-gradient(110deg, rgba(255,255,255,0.99) 0, rgba(255,255,255,0.88) 100%);
|
background-image: linear-gradient(110deg, rgba(255,255,255,0.99) 20%, rgba(255,255,255,0.88) 100%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
) do %>
|
) do %>
|
||||||
<%= method("#{@type}_field").call :setting, @key,
|
<%= method("#{@type}_field").call :setting, @key,
|
||||||
value: Setting.public_send(@key),
|
value: Setting.public_send(@key),
|
||||||
|
placeholder: @placeholder,
|
||||||
data: {
|
data: {
|
||||||
:'default-value' => Setting.get_field(@key)[:default]
|
:'default-value' => Setting.get_field(@key)[:default]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
module FormElements
|
module FormElements
|
||||||
class FieldsetResettableSettingComponent < ViewComponent::Base
|
class FieldsetResettableSettingComponent < ViewComponent::Base
|
||||||
def initialize(tag: "li", key:, type: :text, title:, description: nil)
|
def initialize(tag: "li", key:, type: :text, title:, description: nil, placeholder: nil)
|
||||||
@tag = tag
|
@tag = tag
|
||||||
@positioning = :vertical
|
@positioning = :vertical
|
||||||
@title = title
|
@title = title
|
||||||
@@ -10,6 +10,7 @@ module FormElements
|
|||||||
@key = key.to_sym
|
@key = key.to_sym
|
||||||
@type = type
|
@type = type
|
||||||
@resettable = is_resettable?(@key)
|
@resettable = is_resettable?(@key)
|
||||||
|
@placeholder = placeholder
|
||||||
end
|
end
|
||||||
|
|
||||||
def is_resettable?(key)
|
def is_resettable?(key)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<label class="font-bold mb-1"><%= @title %></label>
|
<label class="font-bold mb-1"><%= @title %></label>
|
||||||
<% if @description.present? %>
|
<% if @description.present? %>
|
||||||
<p class="text-gray-500"><%= @descripton %></p>
|
<p class="text-gray-500"><%= @description %></p>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative ml-4 inline-flex flex-shrink-0">
|
<div class="relative ml-4 inline-flex flex-shrink-0">
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ module FormElements
|
|||||||
@enabled = enabled
|
@enabled = enabled
|
||||||
@input_enabled = input_enabled
|
@input_enabled = input_enabled
|
||||||
@title = title
|
@title = title
|
||||||
@descripton = description
|
@description = description
|
||||||
@button_text = @enabled ? "Switch off" : "Switch on"
|
@button_text = @enabled ? "Switch off" : "Switch on"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -63,4 +63,9 @@ class ApplicationController < ActionController::Base
|
|||||||
@fetch_balance_retried = true
|
@fetch_balance_retried = true
|
||||||
lndhub_fetch_balance
|
lndhub_fetch_balance
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def nostr_event_from_params
|
||||||
|
params.permit!
|
||||||
|
params[:signed_event].to_h.symbolize_keys
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
class LnurlpayController < ApplicationController
|
class LnurlpayController < ApplicationController
|
||||||
before_action :check_service_available
|
before_action :check_service_available
|
||||||
before_action :find_user
|
before_action :find_user
|
||||||
|
before_action :set_cors_access_control_headers, only: [:invoice]
|
||||||
|
|
||||||
MIN_SATS = 10
|
MIN_SATS = 10
|
||||||
MAX_SATS = 1_000_000
|
MAX_SATS = 1_000_000
|
||||||
MAX_COMMENT_CHARS = 100
|
MAX_COMMENT_CHARS = 100
|
||||||
|
|
||||||
|
# GET /.well-known/lnurlp/:username
|
||||||
def index
|
def index
|
||||||
render json: {
|
res = {
|
||||||
status: "OK",
|
status: "OK",
|
||||||
callback: "https://#{Setting.accounts_domain}/lnurlpay/#{@user.cn}/invoice",
|
callback: "https://#{Setting.accounts_domain}/lnurlpay/#{@user.cn}/invoice",
|
||||||
tag: "payRequest",
|
tag: "payRequest",
|
||||||
@@ -16,8 +18,16 @@ class LnurlpayController < ApplicationController
|
|||||||
metadata: metadata(@user.address),
|
metadata: metadata(@user.address),
|
||||||
commentAllowed: MAX_COMMENT_CHARS
|
commentAllowed: MAX_COMMENT_CHARS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if Setting.nostr_enabled?
|
||||||
|
res[:allowsNostr] = true
|
||||||
|
res[:nostrPubkey] = Setting.nostr_public_key
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: res
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# GET /.well-known/keysend/:username
|
||||||
def keysend
|
def keysend
|
||||||
http_status :not_found and return unless Setting.lndhub_keysend_enabled?
|
http_status :not_found and return unless Setting.lndhub_keysend_enabled?
|
||||||
|
|
||||||
@@ -32,8 +42,9 @@ class LnurlpayController < ApplicationController
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# GET /lnurlpay/:username/invoice
|
||||||
def invoice
|
def invoice
|
||||||
amount = params[:amount].to_i / 1000 # msats
|
amount = params[:amount].to_i / 1000 # msats to sats
|
||||||
comment = params[:comment] || ""
|
comment = params[:comment] || ""
|
||||||
address = @user.address
|
address = @user.address
|
||||||
|
|
||||||
@@ -42,53 +53,109 @@ class LnurlpayController < ApplicationController
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if !valid_comment?(comment)
|
if params[:nostr].present? && Setting.nostr_enabled?
|
||||||
render json: { status: "ERROR", reason: "Comment too long" }
|
handle_zap_request amount, params[:nostr], params[:lnurl]
|
||||||
return
|
else
|
||||||
|
handle_pay_request address, amount, comment
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_cors_access_control_headers
|
||||||
|
headers['Access-Control-Allow-Origin'] = "*"
|
||||||
|
headers['Access-Control-Allow-Headers'] = "*"
|
||||||
|
headers['Access-Control-Allow-Methods'] = "GET"
|
||||||
end
|
end
|
||||||
|
|
||||||
memo = "To #{address}"
|
def check_service_available
|
||||||
memo = "#{memo}: \"#{comment}\"" if comment.present?
|
http_status :not_found unless Setting.lndhub_enabled?
|
||||||
|
end
|
||||||
|
|
||||||
payment_request = @user.ln_create_invoice({
|
def find_user
|
||||||
amount: amount, # we create invoices in sats
|
@user = User.where(cn: params[:username], ou: Setting.primary_domain).first
|
||||||
memo: memo,
|
http_status :not_found if @user.nil?
|
||||||
description_hash: Digest::SHA2.hexdigest(metadata(address)),
|
end
|
||||||
})
|
|
||||||
|
|
||||||
render json: {
|
def metadata(address)
|
||||||
status: "OK",
|
"[[\"text/identifier\",\"#{address}\"],[\"text/plain\",\"Sats for #{address}\"]]"
|
||||||
successAction: {
|
end
|
||||||
tag: "message",
|
|
||||||
message: "Sats received. Thank you!"
|
|
||||||
},
|
|
||||||
routes: [],
|
|
||||||
pr: payment_request
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
def valid_amount?(amount_in_sats)
|
||||||
|
amount_in_sats <= MAX_SATS && amount_in_sats >= MIN_SATS
|
||||||
|
end
|
||||||
|
|
||||||
def find_user
|
def valid_comment?(comment)
|
||||||
@user = User.where(cn: params[:username], ou: Setting.primary_domain).first
|
comment.length <= MAX_COMMENT_CHARS
|
||||||
http_status :not_found if @user.nil?
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def metadata(address)
|
def handle_pay_request(address, amount, comment)
|
||||||
"[[\"text/identifier\", \"#{address}\"], [\"text/plain\", \"Send sats, receive thanks.\"]]"
|
if !valid_comment?(comment)
|
||||||
end
|
render json: { status: "ERROR", reason: "Comment too long" }
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
def valid_amount?(amount_in_sats)
|
desc = "To #{address}"
|
||||||
amount_in_sats <= MAX_SATS && amount_in_sats >= MIN_SATS
|
desc = "#{desc}: \"#{comment}\"" if comment.present?
|
||||||
end
|
|
||||||
|
|
||||||
def valid_comment?(comment)
|
invoice = LndhubManager::CreateUserInvoice.call(
|
||||||
comment.length <= MAX_COMMENT_CHARS
|
user: @user, payload: {
|
||||||
end
|
amount: amount, # sats
|
||||||
|
description: desc,
|
||||||
|
description_hash: Digest::SHA256.hexdigest(metadata(address)),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
private
|
render json: {
|
||||||
|
status: "OK",
|
||||||
|
successAction: {
|
||||||
|
tag: "message",
|
||||||
|
message: "Sats received. Thank you!"
|
||||||
|
},
|
||||||
|
routes: [],
|
||||||
|
pr: invoice["payment_request"]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def check_service_available
|
def nostr_event_from_payload(nostr_param)
|
||||||
http_status :not_found unless Setting.lndhub_enabled?
|
event_obj = JSON.parse(nostr_param).transform_keys(&:to_sym)
|
||||||
end
|
Nostr::Event.new(**event_obj)
|
||||||
|
rescue => e
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_zap_request?(amount, event, lnurl)
|
||||||
|
NostrManager::VerifyZapRequest.call(
|
||||||
|
amount: amount, event: event, lnurl: lnurl
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_zap_request(amount, nostr_param, lnurl_param)
|
||||||
|
event = nostr_event_from_payload(nostr_param)
|
||||||
|
|
||||||
|
unless event.present? && valid_zap_request?(amount*1000, event, lnurl_param)
|
||||||
|
render json: { status: "ERROR", reason: "Invalid zap request" }
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO might want to use the existing invoice and zap record if there are
|
||||||
|
# multiple calls with the same zap request
|
||||||
|
|
||||||
|
desc = "Zap for #{@user.address}"
|
||||||
|
desc = "#{desc}: \"#{event.content}\"" if event.content.present?
|
||||||
|
|
||||||
|
invoice = LndhubManager::CreateUserInvoice.call(
|
||||||
|
user: @user, payload: {
|
||||||
|
amount: amount, # sats
|
||||||
|
description: desc,
|
||||||
|
description_hash: Digest::SHA256.hexdigest(event.to_json),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@user.zaps.create! request: event,
|
||||||
|
payment_request: invoice["payment_request"],
|
||||||
|
amount: amount
|
||||||
|
|
||||||
|
render json: { status: "OK", pr: invoice["payment_request"] }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ class Services::ChatController < Services::BaseController
|
|||||||
before_action :require_service_available
|
before_action :require_service_available
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@service_enabled = current_user.services_enabled.include?(:xmpp)
|
@service_enabled = current_user.service_enabled?(:xmpp)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ class Services::MastodonController < Services::BaseController
|
|||||||
before_action :require_service_available
|
before_action :require_service_available
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@service_enabled = current_user.services_enabled.include?(:mastodon)
|
@service_enabled = current_user.service_enabled?(:mastodon)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class Services::RemotestorageController < Services::BaseController
|
|||||||
|
|
||||||
# Dashboard
|
# Dashboard
|
||||||
def show
|
def show
|
||||||
# unless current_user.services_enabled.include?(:remotestorage)
|
# unless current_user.service_enabled?(:remotestorage)
|
||||||
# redirect_to service_remotestorage_info_path
|
# redirect_to service_remotestorage_info_path
|
||||||
# end
|
# end
|
||||||
@rs_auths = current_user.remote_storage_authorizations
|
@rs_auths = current_user.remote_storage_authorizations
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ class SettingsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
if @settings_section == "nostr"
|
case @settings_section
|
||||||
|
when "lightning"
|
||||||
|
@notifications_enabled = @user.preferences[:lightning_notify_sats_received] != "disabled" ||
|
||||||
|
@user.preferences[:lightning_notify_zap_received] != "disabled"
|
||||||
|
when "nostr"
|
||||||
session[:shared_secret] ||= SecureRandom.base64(12)
|
session[:shared_secret] ||= SecureRandom.base64(12)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -87,25 +91,27 @@ class SettingsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def set_nostr_pubkey
|
def set_nostr_pubkey
|
||||||
signed_event = nostr_event_params[:signed_event].to_h.symbolize_keys
|
signed_event = Nostr::Event.new(**nostr_event_from_params)
|
||||||
|
|
||||||
is_valid_id = NostrManager::ValidateId.call(event: signed_event)
|
is_valid_sig = signed_event.verify_signature
|
||||||
is_valid_sig = NostrManager::VerifySignature.call(event: signed_event)
|
is_valid_auth = NostrManager::VerifyAuth.call(
|
||||||
is_correct_content = signed_event[:content] == "Connect my public key to #{current_user.address} (confirmation #{session[:shared_secret]})"
|
event: signed_event,
|
||||||
|
challenge: session[:shared_secret]
|
||||||
|
)
|
||||||
|
|
||||||
unless is_valid_id && is_valid_sig && is_correct_content
|
unless is_valid_sig && is_valid_auth
|
||||||
flash[:alert] = "Public key could not be verified"
|
flash[:alert] = "Public key could not be verified"
|
||||||
http_status :unprocessable_entity and return
|
http_status :unprocessable_entity and return
|
||||||
end
|
end
|
||||||
|
|
||||||
user_with_pubkey = LdapManager::FetchUserByNostrKey.call(pubkey: signed_event[:pubkey])
|
user_with_pubkey = LdapManager::FetchUserByNostrKey.call(pubkey: signed_event.pubkey)
|
||||||
|
|
||||||
if user_with_pubkey.present? && (user_with_pubkey != current_user)
|
if user_with_pubkey.present? && (user_with_pubkey != current_user)
|
||||||
flash[:alert] = "Public key already in use for a different account"
|
flash[:alert] = "Public key already in use for a different account"
|
||||||
http_status :unprocessable_entity and return
|
http_status :unprocessable_entity and return
|
||||||
end
|
end
|
||||||
|
|
||||||
LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: signed_event[:pubkey])
|
LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: signed_event.pubkey)
|
||||||
session[:shared_secret] = nil
|
session[:shared_secret] = nil
|
||||||
|
|
||||||
flash[:success] = "Public key verification successful"
|
flash[:success] = "Public key verification successful"
|
||||||
@@ -145,11 +151,9 @@ class SettingsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
params.require(:user).permit(:display_name, :avatar, preferences: [
|
params.require(:user).permit(
|
||||||
:lightning_notify_sats_received,
|
:display_name, :avatar, preferences: UserPreferences.pref_keys
|
||||||
:remotestorage_notify_auth_created,
|
)
|
||||||
:xmpp_exchange_contacts_with_invitees
|
|
||||||
])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def email_params
|
def email_params
|
||||||
@@ -160,12 +164,6 @@ class SettingsController < ApplicationController
|
|||||||
params.require(:user).permit(:current_password)
|
params.require(:user).permit(:current_password)
|
||||||
end
|
end
|
||||||
|
|
||||||
def nostr_event_params
|
|
||||||
params.permit(signed_event: [
|
|
||||||
:id, :pubkey, :created_at, :kind, :content, :sig, tags: []
|
|
||||||
])
|
|
||||||
end
|
|
||||||
|
|
||||||
def generate_email_password
|
def generate_email_password
|
||||||
characters = [('a'..'z'), ('A'..'Z'), (0..9)].map(&:to_a).flatten
|
characters = [('a'..'z'), ('A'..'Z'), (0..9)].map(&:to_a).flatten
|
||||||
SecureRandom.random_bytes(16).each_byte.map { |b| characters[b % characters.length] }.join
|
SecureRandom.random_bytes(16).each_byte.map { |b| characters[b % characters.length] }.join
|
||||||
|
|||||||
62
app/controllers/users/sessions_controller.rb
Normal file
62
app/controllers/users/sessions_controller.rb
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Users::SessionsController < Devise::SessionsController
|
||||||
|
# before_action :configure_sign_in_params, only: [:create]
|
||||||
|
|
||||||
|
# GET /resource/sign_in
|
||||||
|
def new
|
||||||
|
session[:shared_secret] = SecureRandom.base64(12)
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /resource/sign_in
|
||||||
|
# def create
|
||||||
|
# super
|
||||||
|
# end
|
||||||
|
|
||||||
|
# DELETE /resource/sign_out
|
||||||
|
# def destroy
|
||||||
|
# super
|
||||||
|
# end
|
||||||
|
|
||||||
|
# POST /users/nostr_login
|
||||||
|
def nostr_login
|
||||||
|
signed_event = Nostr::Event.new(**nostr_event_from_params)
|
||||||
|
|
||||||
|
is_valid_sig = signed_event.verify_signature
|
||||||
|
is_valid_auth = NostrManager::VerifyAuth.call(
|
||||||
|
event: signed_event,
|
||||||
|
challenge: session[:shared_secret]
|
||||||
|
)
|
||||||
|
|
||||||
|
session[:shared_secret] = nil
|
||||||
|
|
||||||
|
unless is_valid_sig && is_valid_auth
|
||||||
|
flash[:alert] = "Login verification failed"
|
||||||
|
http_status :unauthorized and return
|
||||||
|
end
|
||||||
|
|
||||||
|
user = LdapManager::FetchUserByNostrKey.call(pubkey: signed_event.pubkey)
|
||||||
|
|
||||||
|
if user.present?
|
||||||
|
set_flash_message!(:notice, :signed_in)
|
||||||
|
sign_in("user", user)
|
||||||
|
render json: { redirect_url: after_sign_in_path_for(user) }, status: :ok
|
||||||
|
else
|
||||||
|
flash[:alert] = "Failed to find your account. Nostr login may be disabled."
|
||||||
|
http_status :unauthorized
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def set_flash_message(key, kind, options = {})
|
||||||
|
# Hide flash message after redirecting from a signin route while logged in
|
||||||
|
super unless key == :alert && kind == "already_authenticated"
|
||||||
|
end
|
||||||
|
|
||||||
|
# If you have extra params to permit, append them to the sanitizer.
|
||||||
|
# def configure_sign_in_params
|
||||||
|
# devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute])
|
||||||
|
# end
|
||||||
|
end
|
||||||
@@ -7,14 +7,15 @@ class WebfingerController < ApplicationController
|
|||||||
resource = params[:resource]
|
resource = params[:resource]
|
||||||
|
|
||||||
if resource && @useraddress = resource.match(/acct:(.+)/)&.[](1)
|
if resource && @useraddress = resource.match(/acct:(.+)/)&.[](1)
|
||||||
@username, @org = @useraddress.split("@")
|
@username, @domain = @useraddress.split("@")
|
||||||
|
|
||||||
unless Rails.env.development?
|
unless Rails.env.development?
|
||||||
# Allow different domains (e.g. localhost:3000) in development only
|
# Allow different domains (e.g. localhost:3000) in development only
|
||||||
head 404 and return unless @org == Setting.primary_domain
|
head 404 and return unless @domain == Setting.primary_domain
|
||||||
end
|
end
|
||||||
|
|
||||||
unless User.where(cn: @username.downcase, ou: Setting.primary_domain).any?
|
unless @user = User.where(ou: Setting.primary_domain)
|
||||||
|
.find_by(cn: @username.downcase)
|
||||||
head 404 and return
|
head 404 and return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -28,12 +29,50 @@ class WebfingerController < ApplicationController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def webfinger
|
def webfinger
|
||||||
links = [];
|
jrd = {
|
||||||
|
subject: "acct:#{@user.address}",
|
||||||
|
aliases: [],
|
||||||
|
links: []
|
||||||
|
}
|
||||||
|
|
||||||
# TODO check if storage service is enabled for user, not just globally
|
if Setting.mastodon_enabled && @user.service_enabled?(:mastodon)
|
||||||
links << remotestorage_link if Setting.remotestorage_enabled
|
# https://docs.joinmastodon.org/spec/webfinger/
|
||||||
|
jrd[:aliases] += mastodon_aliases
|
||||||
|
jrd[:links] += mastodon_links
|
||||||
|
end
|
||||||
|
|
||||||
{ "links" => links }
|
if Setting.remotestorage_enabled && @user.service_enabled?(:remotestorage)
|
||||||
|
# https://datatracker.ietf.org/doc/draft-dejong-remotestorage/
|
||||||
|
jrd[:links] << remotestorage_link
|
||||||
|
end
|
||||||
|
|
||||||
|
jrd
|
||||||
|
end
|
||||||
|
|
||||||
|
def mastodon_aliases
|
||||||
|
[
|
||||||
|
"#{Setting.mastodon_public_url}/@#{@user.cn}",
|
||||||
|
"#{Setting.mastodon_public_url}/users/#{@user.cn}"
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def mastodon_links
|
||||||
|
[
|
||||||
|
{
|
||||||
|
rel: "http://webfinger.net/rel/profile-page",
|
||||||
|
type: "text/html",
|
||||||
|
href: "#{Setting.mastodon_public_url}/@#{@user.cn}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rel: "self",
|
||||||
|
type: "application/activity+json",
|
||||||
|
href: "#{Setting.mastodon_public_url}/users/#{@user.cn}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rel: "http://ostatus.org/schema/1.0/subscribe",
|
||||||
|
template: "#{Setting.mastodon_public_url}/authorize_interaction?uri={uri}"
|
||||||
|
}
|
||||||
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
def remotestorage_link
|
def remotestorage_link
|
||||||
@@ -41,9 +80,9 @@ class WebfingerController < ApplicationController
|
|||||||
storage_url = "#{Setting.rs_storage_url}/#{@username}"
|
storage_url = "#{Setting.rs_storage_url}/#{@username}"
|
||||||
|
|
||||||
{
|
{
|
||||||
"rel" => "http://tools.ietf.org/id/draft-dejong-remotestorage",
|
rel: "http://tools.ietf.org/id/draft-dejong-remotestorage",
|
||||||
"href" => storage_url,
|
href: storage_url,
|
||||||
"properties" => {
|
properties: {
|
||||||
"http://remotestorage.io/spec/version" => "draft-dejong-remotestorage-13",
|
"http://remotestorage.io/spec/version" => "draft-dejong-remotestorage-13",
|
||||||
"http://tools.ietf.org/html/rfc6749#section-4.2" => auth_url,
|
"http://tools.ietf.org/html/rfc6749#section-4.2" => auth_url,
|
||||||
"http://tools.ietf.org/html/rfc6750#section-2.3" => nil, # access token via a HTTP query parameter
|
"http://tools.ietf.org/html/rfc6750#section-2.3" => nil, # access token via a HTTP query parameter
|
||||||
|
|||||||
@@ -2,45 +2,76 @@ class WebhooksController < ApplicationController
|
|||||||
skip_forgery_protection
|
skip_forgery_protection
|
||||||
|
|
||||||
before_action :authorize_request
|
before_action :authorize_request
|
||||||
|
before_action :process_payload
|
||||||
|
|
||||||
def lndhub
|
def lndhub
|
||||||
begin
|
@user = User.find_by!(ln_account: @payload[:user_login])
|
||||||
payload = JSON.parse(request.body.read, symbolize_names: true)
|
|
||||||
head :no_content and return unless payload[:type] == "incoming"
|
if @zap = @user.zaps.find_by(payment_request: @payload[:payment_request])
|
||||||
rescue
|
settled_at = Time.parse(@payload[:settled_at])
|
||||||
head :unprocessable_entity and return
|
zap_receipt = NostrManager::CreateZapReceipt.call(
|
||||||
|
zap: @zap,
|
||||||
|
paid_at: settled_at.to_i,
|
||||||
|
preimage: @payload[:preimage]
|
||||||
|
)
|
||||||
|
@zap.update! settled_at: settled_at, receipt: zap_receipt.to_h
|
||||||
|
NostrManager::PublishZapReceipt.call(zap: @zap)
|
||||||
end
|
end
|
||||||
|
|
||||||
user = User.find_by!(ln_account: payload[:user_login])
|
send_notifications
|
||||||
notify = user.preferences[:lightning_notify_sats_received]
|
|
||||||
case notify
|
|
||||||
when "xmpp"
|
|
||||||
notify_xmpp(user.address, payload[:amount], payload[:memo])
|
|
||||||
when "email"
|
|
||||||
NotificationMailer.with(user: user, amount_sats: payload[:amount])
|
|
||||||
.lightning_sats_received.deliver_later
|
|
||||||
end
|
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# TODO refactor into mailer-like generic class/service
|
|
||||||
def notify_xmpp(address, amt_sats, memo)
|
|
||||||
payload = {
|
|
||||||
type: "normal",
|
|
||||||
from: Setting.xmpp_notifications_from_address,
|
|
||||||
to: address,
|
|
||||||
subject: "Sats received!",
|
|
||||||
body: "#{helpers.number_with_delimiter amt_sats} sats received in your Lightning wallet:\n> #{memo}"
|
|
||||||
}
|
|
||||||
XmppSendMessageJob.perform_later(payload)
|
|
||||||
end
|
|
||||||
|
|
||||||
def authorize_request
|
def authorize_request
|
||||||
if !ENV['WEBHOOKS_ALLOWED_IPS'].split(',').include?(request.remote_ip)
|
if !ENV['WEBHOOKS_ALLOWED_IPS'].split(',').include?(request.remote_ip)
|
||||||
head :forbidden and return
|
head :forbidden and return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def process_payload
|
||||||
|
@payload = JSON.parse(request.body.read, symbolize_names: true)
|
||||||
|
unless @payload[:type] == "incoming" &&
|
||||||
|
@payload[:state] == "settled"
|
||||||
|
head :no_content and return
|
||||||
|
end
|
||||||
|
rescue
|
||||||
|
head :unprocessable_entity and return
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_notifications
|
||||||
|
return if @payload[:amount] < @user.preferences[:lightning_notify_min_sats]
|
||||||
|
|
||||||
|
if @user.preferences[:lightning_notify_only_with_message]
|
||||||
|
return if @payload[:memo].blank?
|
||||||
|
end
|
||||||
|
|
||||||
|
target = @zap.present? ? @user.preferences[:lightning_notify_zap_received] :
|
||||||
|
@user.preferences[:lightning_notify_sats_received]
|
||||||
|
|
||||||
|
case target
|
||||||
|
when "xmpp"
|
||||||
|
notify_xmpp
|
||||||
|
when "email"
|
||||||
|
notify_email
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO refactor into mailer-like generic class/service
|
||||||
|
def notify_xmpp
|
||||||
|
XmppSendMessageJob.perform_later({
|
||||||
|
type: "normal",
|
||||||
|
from: Setting.xmpp_notifications_from_address,
|
||||||
|
to: @user.address,
|
||||||
|
subject: "Sats received!",
|
||||||
|
body: "#{helpers.number_with_delimiter @payload[:amount]} sats received in your Lightning wallet:\n> #{@payload[:memo]}"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
def notify_email
|
||||||
|
NotificationMailer.with(user: @user, amount_sats: @payload[:amount])
|
||||||
|
.lightning_sats_received.deliver_later
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,16 +1,33 @@
|
|||||||
class WellKnownController < ApplicationController
|
class WellKnownController < ApplicationController
|
||||||
|
before_action :require_nostr_enabled, only: [ :nostr ]
|
||||||
|
|
||||||
def nostr
|
def nostr
|
||||||
http_status :unprocessable_entity and return if params[:name].blank?
|
http_status :unprocessable_entity and return if params[:name].blank?
|
||||||
domain = request.headers["X-Forwarded-Host"].presence || Setting.primary_domain
|
domain = request.headers["X-Forwarded-Host"].presence || Setting.primary_domain
|
||||||
@user = User.where(cn: params[:name], ou: domain).first
|
relay_url = Setting.nostr_relay_url
|
||||||
http_status :not_found and return if @user.nil? || @user.nostr_pubkey.blank?
|
|
||||||
|
if params[:name] == "_"
|
||||||
|
# pubkey for the primary domain without a username (e.g. kosmos.org)
|
||||||
|
res = { names: { "_": Setting.nostr_public_key } }
|
||||||
|
res[:relays] = { "_" => [ relay_url ] } if relay_url
|
||||||
|
else
|
||||||
|
@user = User.where(cn: params[:name], ou: domain).first
|
||||||
|
http_status :not_found and return if @user.nil? || @user.nostr_pubkey.blank?
|
||||||
|
|
||||||
|
res = { names: { @user.cn => @user.nostr_pubkey } }
|
||||||
|
res[:relays] = { @user.nostr_pubkey => [ relay_url ] } if relay_url
|
||||||
|
end
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.json do
|
format.json do
|
||||||
render json: {
|
render json: res.to_json
|
||||||
names: { "#{@user.cn}": @user.nostr_pubkey }
|
|
||||||
}.to_json
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def require_nostr_enabled
|
||||||
|
http_status :not_found unless Setting.nostr_enabled?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
53
app/javascript/controllers/nostr_login_controller.js
Normal file
53
app/javascript/controllers/nostr_login_controller.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
// Connects to data-controller="nostr-login"
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = [ "loginForm", "loginButton" ]
|
||||||
|
static values = { site: String, sharedSecret: String }
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
if (window.nostr) {
|
||||||
|
this.loginButtonTarget.disabled = false
|
||||||
|
this.loginFormTarget.classList.remove("hidden")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async login () {
|
||||||
|
this.loginButtonTarget.disabled = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Auth based on NIP-42
|
||||||
|
const signedEvent = await window.nostr.signEvent({
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
kind: 22242,
|
||||||
|
tags: [
|
||||||
|
["site", this.siteValue],
|
||||||
|
["challenge", this.sharedSecretValue]
|
||||||
|
],
|
||||||
|
content: ""
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await fetch("/users/nostr_login", {
|
||||||
|
method: "POST", credentials: "include", headers: {
|
||||||
|
"Accept": "application/json", 'Content-Type': 'application/json',
|
||||||
|
"X-CSRF-Token": this.csrfToken
|
||||||
|
}, body: JSON.stringify({ signed_event: signedEvent })
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
res.json().then(r => { window.location.href = r.redirect_url })
|
||||||
|
} else {
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Unable to authenticate:', error.message)
|
||||||
|
} finally {
|
||||||
|
this.loginButtonTarget.disabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get csrfToken () {
|
||||||
|
const element = document.head.querySelector('meta[name="csrf-token"]')
|
||||||
|
return element.getAttribute("content")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,12 @@ import { Controller } from "@hotwired/stimulus"
|
|||||||
// Connects to data-controller="settings--nostr-pubkey"
|
// Connects to data-controller="settings--nostr-pubkey"
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = [ "noExtension", "setPubkey", "pubkeyBech32Input" ]
|
static targets = [ "noExtension", "setPubkey", "pubkeyBech32Input" ]
|
||||||
static values = { userAddress: String, pubkeyHex: String, sharedSecret: String }
|
static values = {
|
||||||
|
userAddress: String,
|
||||||
|
pubkeyHex: String,
|
||||||
|
site: String,
|
||||||
|
sharedSecret: String
|
||||||
|
}
|
||||||
|
|
||||||
connect () {
|
connect () {
|
||||||
if (window.nostr) {
|
if (window.nostr) {
|
||||||
@@ -19,11 +24,15 @@ export default class extends Controller {
|
|||||||
this.setPubkeyTarget.disabled = true
|
this.setPubkeyTarget.disabled = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Auth based on NIP-42
|
||||||
const signedEvent = await window.nostr.signEvent({
|
const signedEvent = await window.nostr.signEvent({
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
kind: 1,
|
kind: 22242,
|
||||||
tags: [],
|
tags: [
|
||||||
content: `Connect my public key to ${this.userAddressValue} (confirmation ${this.sharedSecretValue})`
|
["site", this.siteValue],
|
||||||
|
["challenge", this.sharedSecretValue]
|
||||||
|
],
|
||||||
|
content: ""
|
||||||
})
|
})
|
||||||
|
|
||||||
const res = await fetch("/settings/set_nostr_pubkey", {
|
const res = await fetch("/settings/set_nostr_pubkey", {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
class CreateLdapUserJob < ApplicationJob
|
class CreateLdapUserJob < ApplicationJob
|
||||||
queue_as :default
|
queue_as :default
|
||||||
|
|
||||||
def perform(username, domain, email, hashed_pw)
|
def perform(username:, domain:, email:, hashed_pw:, confirmed: false)
|
||||||
dn = "cn=#{username},ou=#{domain},cn=users,dc=kosmos,dc=org"
|
dn = "cn=#{username},ou=#{domain},cn=users,dc=kosmos,dc=org"
|
||||||
attr = {
|
attr = {
|
||||||
objectclass: ["top", "account", "person", "extensibleObject"],
|
objectclass: ["top", "account", "person", "extensibleObject"],
|
||||||
@@ -12,6 +12,10 @@ class CreateLdapUserJob < ApplicationJob
|
|||||||
userPassword: hashed_pw
|
userPassword: hashed_pw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if confirmed
|
||||||
|
attr[:serviceEnabled] = Setting.default_services
|
||||||
|
end
|
||||||
|
|
||||||
ldap_client.add(dn: dn, attributes: attr)
|
ldap_client.add(dn: dn, attributes: attr)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
7
app/jobs/nostr_publish_event_job.rb
Normal file
7
app/jobs/nostr_publish_event_job.rb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
class NostrPublishEventJob < ApplicationJob
|
||||||
|
queue_as :nostr
|
||||||
|
|
||||||
|
def perform(event:, relay_url:)
|
||||||
|
NostrManager::PublishEvent.call(event: event, relay_url: relay_url)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -2,8 +2,8 @@ class XmppExchangeContactsJob < ApplicationJob
|
|||||||
queue_as :default
|
queue_as :default
|
||||||
|
|
||||||
def perform(inviter, invitee)
|
def perform(inviter, invitee)
|
||||||
return unless inviter.services_enabled.include?("xmpp") &&
|
return unless inviter.service_enabled?(:xmpp) &&
|
||||||
invitee.services_enabled.include?("xmpp") &&
|
invitee.service_enabled?(:xmpp) &&
|
||||||
inviter.preferences[:xmpp_exchange_contacts_with_invitees]
|
inviter.preferences[:xmpp_exchange_contacts_with_invitees]
|
||||||
|
|
||||||
ejabberd = EjabberdApiClient.new
|
ejabberd = EjabberdApiClient.new
|
||||||
|
|||||||
@@ -160,7 +160,20 @@ class Setting < RailsSettings::Base
|
|||||||
# Nostr
|
# Nostr
|
||||||
#
|
#
|
||||||
|
|
||||||
field :nostr_enabled, type: :boolean, default: false
|
field :nostr_enabled, type: :boolean,
|
||||||
|
default: ENV["NOSTR_PRIVATE_KEY"].present?
|
||||||
|
|
||||||
|
field :nostr_private_key, type: :string,
|
||||||
|
default: ENV["NOSTR_PRIVATE_KEY"].presence
|
||||||
|
|
||||||
|
field :nostr_public_key, type: :string,
|
||||||
|
default: ENV["NOSTR_PUBLIC_KEY"].presence
|
||||||
|
|
||||||
|
field :nostr_relay_url, type: :string,
|
||||||
|
default: ENV["NOSTR_RELAY_URL"].presence
|
||||||
|
|
||||||
|
field :nostr_zaps_relay_limit, type: :integer,
|
||||||
|
default: 12
|
||||||
|
|
||||||
#
|
#
|
||||||
# OpenCollective
|
# OpenCollective
|
||||||
@@ -206,4 +219,9 @@ class Setting < RailsSettings::Base
|
|||||||
#
|
#
|
||||||
# field :email_imap_port, type: :string,
|
# field :email_imap_port, type: :string,
|
||||||
# default: ENV["EMAIL_IMAP_PORT"].presence || 993
|
# default: ENV["EMAIL_IMAP_PORT"].presence || 993
|
||||||
|
|
||||||
|
def self.default_services
|
||||||
|
# TODO Make configurable from respective service settings page
|
||||||
|
%w[ discourse gitea mastodon mediawiki xmpp ]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,16 +17,15 @@ class User < ApplicationRecord
|
|||||||
has_one :invitation, inverse_of: :invitee, foreign_key: 'invited_user_id'
|
has_one :invitation, inverse_of: :invitee, foreign_key: 'invited_user_id'
|
||||||
has_one :inviter, through: :invitation, source: :user
|
has_one :inviter, through: :invitation, source: :user
|
||||||
has_many :invitees, through: :invitations
|
has_many :invitees, through: :invitations
|
||||||
|
|
||||||
has_many :donations, dependent: :nullify
|
has_many :donations, dependent: :nullify
|
||||||
|
has_many :remote_storage_authorizations
|
||||||
|
has_many :zaps
|
||||||
|
|
||||||
has_one :lndhub_user, class_name: "LndhubUser", inverse_of: "user",
|
has_one :lndhub_user, class_name: "LndhubUser", inverse_of: "user",
|
||||||
primary_key: "ln_account", foreign_key: "login"
|
primary_key: "ln_account", foreign_key: "login"
|
||||||
|
|
||||||
has_many :accounts, through: :lndhub_user
|
has_many :accounts, through: :lndhub_user
|
||||||
|
|
||||||
has_many :remote_storage_authorizations
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Validations
|
# Validations
|
||||||
#
|
#
|
||||||
@@ -93,9 +92,7 @@ class User < ApplicationRecord
|
|||||||
LdapManager::UpdateEmail.call(dn: self.dn, address: self.email)
|
LdapManager::UpdateEmail.call(dn: self.dn, address: self.email)
|
||||||
else
|
else
|
||||||
# E-Mail from signup confirmed (i.e. account activation)
|
# E-Mail from signup confirmed (i.e. account activation)
|
||||||
|
enable_default_services
|
||||||
# TODO Make configurable, only activate globally enabled services
|
|
||||||
enable_service %w[ discourse gitea mediawiki xmpp ]
|
|
||||||
|
|
||||||
# TODO enable in development when we have easy setup of ejabberd etc.
|
# TODO enable in development when we have easy setup of ejabberd etc.
|
||||||
return if Rails.env.development? || !Setting.ejabberd_enabled?
|
return if Rails.env.development? || !Setting.ejabberd_enabled?
|
||||||
@@ -133,7 +130,7 @@ class User < ApplicationRecord
|
|||||||
|
|
||||||
def mastodon_address
|
def mastodon_address
|
||||||
return nil unless Setting.mastodon_enabled?
|
return nil unless Setting.mastodon_enabled?
|
||||||
"#{self.cn}@#{Setting.mastodon_address_domain}"
|
"#{self.cn.gsub("-", "_")}@#{Setting.mastodon_address_domain}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def valid_attribute?(attribute_name)
|
def valid_attribute?(attribute_name)
|
||||||
@@ -141,10 +138,8 @@ class User < ApplicationRecord
|
|||||||
self.errors[attribute_name].blank?
|
self.errors[attribute_name].blank?
|
||||||
end
|
end
|
||||||
|
|
||||||
def ln_create_invoice(payload)
|
def enable_default_services
|
||||||
lndhub = Lndhub.new
|
enable_service Setting.default_services
|
||||||
lndhub.authenticate self
|
|
||||||
lndhub.addinvoice payload
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def dn
|
def dn
|
||||||
@@ -178,6 +173,10 @@ class User < ApplicationRecord
|
|||||||
ldap_entry[:services_enabled] || []
|
ldap_entry[:services_enabled] || []
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def service_enabled?(name)
|
||||||
|
services_enabled.map(&:to_sym).include?(name.to_sym)
|
||||||
|
end
|
||||||
|
|
||||||
def enable_service(service)
|
def enable_service(service)
|
||||||
current_services = services_enabled
|
current_services = services_enabled
|
||||||
new_services = Array(service).map(&:to_s)
|
new_services = Array(service).map(&:to_s)
|
||||||
|
|||||||
@@ -26,4 +26,8 @@ class UserPreferences
|
|||||||
end
|
end
|
||||||
hash.stringify_keys!.to_h
|
hash.stringify_keys!.to_h
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.pref_keys
|
||||||
|
DEFAULT_PREFS.keys.map(&:to_sym)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
20
app/models/zap.rb
Normal file
20
app/models/zap.rb
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
class Zap < ApplicationRecord
|
||||||
|
belongs_to :user
|
||||||
|
|
||||||
|
scope :settled, -> { where.not(settled_at: nil) }
|
||||||
|
scope :unpaid, -> { where(settled_at: nil) }
|
||||||
|
|
||||||
|
def request_event
|
||||||
|
nostr_event_from_hash(request)
|
||||||
|
end
|
||||||
|
|
||||||
|
def receipt_event
|
||||||
|
nostr_event_from_hash(receipt)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def nostr_event_from_hash(hash)
|
||||||
|
Nostr::Event.new(**hash.symbolize_keys)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -35,11 +35,15 @@ class CreateAccount < ApplicationService
|
|||||||
@invitation.update! invited_user_id: user_id, used_at: DateTime.now
|
@invitation.update! invited_user_id: user_id, used_at: DateTime.now
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO move to confirmation
|
|
||||||
# (and/or add email_confirmed to entry and use in login filter)
|
|
||||||
def add_ldap_document
|
def add_ldap_document
|
||||||
hashed_pw = Devise.ldap_auth_password_builder.call(@password)
|
hashed_pw = Devise.ldap_auth_password_builder.call(@password)
|
||||||
CreateLdapUserJob.perform_later(@username, @domain, @email, hashed_pw)
|
CreateLdapUserJob.perform_later(
|
||||||
|
username: @username,
|
||||||
|
domain: @domain,
|
||||||
|
email: @email,
|
||||||
|
hashed_pw: hashed_pw,
|
||||||
|
confirmed: @confirmed
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_lndhub_account(user)
|
def create_lndhub_account(user)
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ class LdapService < ApplicationService
|
|||||||
dn = "ou=#{ou},cn=users,#{ldap_suffix}"
|
dn = "ou=#{ou},cn=users,#{ldap_suffix}"
|
||||||
|
|
||||||
aci = <<-EOS
|
aci = <<-EOS
|
||||||
(target="ldap:///cn=*,ou=#{ou},cn=users,#{ldap_suffix}")(targetattr="cn || sn || uid || mail || userPassword || nsRole || objectClass") (version 3.0; acl "service-#{ou.gsub(".", "-")}-read-search"; allow (read,search) userdn="ldap:///uid=service,ou=#{ou},cn=applications,#{ldap_suffix}";)
|
(target="ldap:///cn=*,ou=#{ou},cn=users,#{ldap_suffix}")(targetattr="cn || sn || uid || userPassword || mail || mailRoutingAddress || serviceEnabled || nostrKey || nsRole || objectClass") (version 3.0; acl "service-#{ou.gsub(".", "-")}-read-search"; allow (read,search) userdn="ldap:///uid=service,ou=#{ou},cn=applications,#{ldap_suffix}";)
|
||||||
EOS
|
EOS
|
||||||
|
|
||||||
attrs = {
|
attrs = {
|
||||||
|
|||||||
13
app/services/lndhub_manager/create_user_invoice.rb
Normal file
13
app/services/lndhub_manager/create_user_invoice.rb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
module LndhubManager
|
||||||
|
class CreateUserInvoice < LndhubV2
|
||||||
|
def initialize(user:, payload:)
|
||||||
|
@user = user
|
||||||
|
@payload = payload
|
||||||
|
end
|
||||||
|
|
||||||
|
def call
|
||||||
|
authenticate @user
|
||||||
|
create_invoice @payload
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
25
app/services/nostr_manager/create_zap_receipt.rb
Normal file
25
app/services/nostr_manager/create_zap_receipt.rb
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
module NostrManager
|
||||||
|
class CreateZapReceipt < NostrManagerService
|
||||||
|
def initialize(zap:, paid_at:, preimage:)
|
||||||
|
@zap, @paid_at, @preimage = zap, paid_at, preimage
|
||||||
|
end
|
||||||
|
|
||||||
|
def call
|
||||||
|
request_tags = parse_tags(@zap.request_event.tags)
|
||||||
|
|
||||||
|
site_user.create_event(
|
||||||
|
kind: 9735,
|
||||||
|
created_at: @paid_at,
|
||||||
|
content: "",
|
||||||
|
tags: [
|
||||||
|
["p", request_tags[:p].first],
|
||||||
|
["e", request_tags[:e]&.first],
|
||||||
|
["a", request_tags[:a]&.first],
|
||||||
|
["bolt11", @zap.payment_request],
|
||||||
|
["preimage", @preimage],
|
||||||
|
["description", @zap.request_event.to_json]
|
||||||
|
].reject { |t| t[1].nil? }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
50
app/services/nostr_manager/publish_event.rb
Normal file
50
app/services/nostr_manager/publish_event.rb
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
module NostrManager
|
||||||
|
class PublishEvent < NostrManagerService
|
||||||
|
def initialize(event:, relay_url:)
|
||||||
|
relay_name = URI.parse(relay_url).host
|
||||||
|
@relay = Nostr::Relay.new(url: relay_url, name: relay_name)
|
||||||
|
|
||||||
|
if event.is_a?(Nostr::Event)
|
||||||
|
@event = event
|
||||||
|
else
|
||||||
|
@event = Nostr::Event.new(**event.symbolize_keys)
|
||||||
|
end
|
||||||
|
|
||||||
|
@client = Nostr::Client.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def call
|
||||||
|
client, relay, event = @client, @relay, @event
|
||||||
|
log_prefix = "[nostr][#{relay.name}]"
|
||||||
|
|
||||||
|
thread = Thread.new do
|
||||||
|
client.on :connect do
|
||||||
|
puts "#{log_prefix} Publishing #{event.id}..."
|
||||||
|
client.publish event
|
||||||
|
end
|
||||||
|
|
||||||
|
client.on :error do |e|
|
||||||
|
puts "#{log_prefix} Error: #{e}"
|
||||||
|
puts "#{log_prefix} Closing thread..."
|
||||||
|
thread.exit
|
||||||
|
end
|
||||||
|
|
||||||
|
client.on :message do |m|
|
||||||
|
puts "#{log_prefix} Message: #{m}"
|
||||||
|
msg = JSON.parse(m) rescue []
|
||||||
|
if msg[0] == "OK" && msg[1] == event.id && msg[2]
|
||||||
|
puts "#{log_prefix} Event published. Closing thread..."
|
||||||
|
else
|
||||||
|
puts "#{log_prefix} Unexpected message from relay. Closing thread..."
|
||||||
|
end
|
||||||
|
thread.exit
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "#{log_prefix} Connecting to #{relay.url}..."
|
||||||
|
client.connect relay
|
||||||
|
end
|
||||||
|
|
||||||
|
thread.join
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
24
app/services/nostr_manager/publish_zap_receipt.rb
Normal file
24
app/services/nostr_manager/publish_zap_receipt.rb
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
module NostrManager
|
||||||
|
class PublishZapReceipt < NostrManagerService
|
||||||
|
def initialize(zap:, delayed: true)
|
||||||
|
@zap, @delayed = zap, delayed
|
||||||
|
end
|
||||||
|
|
||||||
|
def call
|
||||||
|
tags = parse_tags(@zap.request_event.tags)
|
||||||
|
relays = tags[:relays].take(Setting.nostr_zaps_relay_limit)
|
||||||
|
|
||||||
|
if Setting.nostr_relay_url.present?
|
||||||
|
relays << Setting.nostr_relay_url
|
||||||
|
end
|
||||||
|
|
||||||
|
relays.uniq.each do |relay_url|
|
||||||
|
if @delayed
|
||||||
|
NostrPublishEventJob.perform_later(event: @zap.receipt, relay_url: relay_url)
|
||||||
|
else
|
||||||
|
NostrManager::PublishEvent.call(event: @zap.receipt_event, relay_url: relay_url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
module NostrManager
|
|
||||||
class ValidateId < NostrManagerService
|
|
||||||
def initialize(event:)
|
|
||||||
@event = Nostr::Event.new(**event)
|
|
||||||
end
|
|
||||||
|
|
||||||
def call
|
|
||||||
@event.id == Digest::SHA256.hexdigest(JSON.generate(@event.serialize))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
18
app/services/nostr_manager/verify_auth.rb
Normal file
18
app/services/nostr_manager/verify_auth.rb
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
module NostrManager
|
||||||
|
class VerifyAuth < NostrManagerService
|
||||||
|
def initialize(event:, challenge:)
|
||||||
|
@event = event
|
||||||
|
@challenge_expected = challenge
|
||||||
|
@site_expected = Setting.accounts_domain
|
||||||
|
end
|
||||||
|
|
||||||
|
def call
|
||||||
|
tags = parse_tags(@event.tags)
|
||||||
|
site_given = tags[:site].first
|
||||||
|
challenge_given = tags[:challenge].first
|
||||||
|
|
||||||
|
site_given == @site_expected &&
|
||||||
|
challenge_given == @challenge_expected
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
module NostrManager
|
|
||||||
class VerifySignature < NostrManagerService
|
|
||||||
def initialize(event:)
|
|
||||||
@event = Nostr::Event.new(**event)
|
|
||||||
end
|
|
||||||
|
|
||||||
def call
|
|
||||||
Schnorr.check_sig!(
|
|
||||||
[@event.id].pack('H*'),
|
|
||||||
[@event.pubkey].pack('H*'),
|
|
||||||
[@event.sig].pack('H*')
|
|
||||||
)
|
|
||||||
rescue Schnorr::InvalidSignatureError
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
51
app/services/nostr_manager/verify_zap_request.rb
Normal file
51
app/services/nostr_manager/verify_zap_request.rb
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
module NostrManager
|
||||||
|
class VerifyZapRequest < NostrManagerService
|
||||||
|
def initialize(amount:, event:, lnurl: nil)
|
||||||
|
@amount, @event, @lnurl = amount, event, lnurl
|
||||||
|
end
|
||||||
|
|
||||||
|
# https://github.com/nostr-protocol/nips/blob/27fef638e2460139cc9078427a0aec0ce4470517/57.md#appendix-d-lnurl-server-zap-request-validation
|
||||||
|
def call
|
||||||
|
tags = parse_tags(@event.tags)
|
||||||
|
|
||||||
|
@event.verify_signature &&
|
||||||
|
@event.kind == 9734 &&
|
||||||
|
tags.present? &&
|
||||||
|
valid_p_tag?(tags[:p]) &&
|
||||||
|
valid_e_tag?(tags[:e]) &&
|
||||||
|
valid_a_tag?(tags[:a]) &&
|
||||||
|
valid_amount_tag?(tags[:amount]) &&
|
||||||
|
valid_lnurl_tag?(tags[:lnurl])
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_p_tag?(tag)
|
||||||
|
return false unless tag.present? && tag.length == 1
|
||||||
|
key = Nostr::PublicKey.new(tag.first) rescue nil
|
||||||
|
key.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_e_tag?(tag)
|
||||||
|
return true unless tag.present?
|
||||||
|
# TODO validate format of event ID properly
|
||||||
|
tag.length == 1 && tag.first.is_a?(String)
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_a_tag?(tag)
|
||||||
|
return true unless tag.present?
|
||||||
|
# TODO validate format of event coordinate properly
|
||||||
|
tag.length == 1 && tag.first.is_a?(String)
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_amount_tag?(tag)
|
||||||
|
return true unless tag.present?
|
||||||
|
amount = tag.first
|
||||||
|
amount.is_a?(String) && amount.to_i == @amount
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_lnurl_tag?(tag)
|
||||||
|
return true unless tag.present?
|
||||||
|
# TODO validate lnurl matching recipient's lnurlp
|
||||||
|
tag.first.is_a?(String)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,4 +1,22 @@
|
|||||||
require "nostr"
|
require "nostr"
|
||||||
|
|
||||||
class NostrManagerService < ApplicationService
|
class NostrManagerService < ApplicationService
|
||||||
|
def parse_tags(tags)
|
||||||
|
out = {}
|
||||||
|
tags.each do |tag|
|
||||||
|
out[tag[0].to_sym] = tag[1, tag.length]
|
||||||
|
end
|
||||||
|
out
|
||||||
|
end
|
||||||
|
|
||||||
|
def site_keypair
|
||||||
|
Nostr::KeyPair.new(
|
||||||
|
private_key: Nostr::PrivateKey.new(Setting.nostr_private_key),
|
||||||
|
public_key: Nostr::PublicKey.new(Setting.nostr_public_key)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def site_user
|
||||||
|
Nostr::User.new(keypair: site_keypair)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -38,8 +38,8 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="overflow-ellipsis font-mono"><%= invitation.token %></td>
|
<td class="overflow-ellipsis font-mono"><%= invitation.token %></td>
|
||||||
<td><%= invitation.used_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
|
<td><%= invitation.used_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
|
||||||
<td><%= link_to invitation.user.address, admin_user_path(invitation.user.address), class: "ks-text-link" %></td>
|
<td><%= link_to invitation.user.cn, admin_user_path(invitation.user.cn), class: "ks-text-link" %></td>
|
||||||
<td><%= link_to invitation.invitee.address, admin_user_path(invitation.invitee.address), class: "ks-text-link" %></td>
|
<td><%= link_to invitation.invitee.cn, admin_user_path(invitation.invitee.cn), class: "ks-text-link" %></td>
|
||||||
</tr>
|
</tr>
|
||||||
<% end %>
|
<% end %>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<% if user = @users.find{ |u| u[2] == account.login } %>
|
<% if user = @users.find{ |u| u[2] == account.login } %>
|
||||||
<%= link_to "#{user[0]}@#{user[1]}", admin_user_path("#{user[0]}@#{user[1]}"), class: "ks-text-link" %>
|
<%= link_to user[0], admin_user_path(user[0]), class: "ks-text-link" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</td>
|
</td>
|
||||||
<td><%= number_with_delimiter account.balance.to_i.to_s %></td>
|
<td><%= number_with_delimiter account.balance.to_i.to_s %></td>
|
||||||
|
|||||||
@@ -7,4 +7,32 @@
|
|||||||
title: "Enable Nostr integration (experimental)",
|
title: "Enable Nostr integration (experimental)",
|
||||||
description: "Allow adding nostr pubkeys and resolve user addresses via NIP-05"
|
description: "Allow adding nostr pubkeys and resolve user addresses via NIP-05"
|
||||||
) %>
|
) %>
|
||||||
|
<% if Setting.nostr_enabled? %>
|
||||||
|
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||||
|
key: :nostr_private_key,
|
||||||
|
type: :password,
|
||||||
|
title: "Private key",
|
||||||
|
description: "The private key of the accounts service, used when publishing events (e.g. zap receipts)"
|
||||||
|
) %>
|
||||||
|
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||||
|
key: :nostr_public_key,
|
||||||
|
title: "Public key",
|
||||||
|
description: "The corresponding public key of the accounts service"
|
||||||
|
) %>
|
||||||
|
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||||
|
key: :nostr_relay_url,
|
||||||
|
title: "Relay URL",
|
||||||
|
description: "Websockets URL of a relay associated with #{Setting.primary_domain}"
|
||||||
|
) %>
|
||||||
</ul>
|
</ul>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3>Zaps</h3>
|
||||||
|
<ul role="list">
|
||||||
|
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||||
|
key: :nostr_zaps_relay_limit,
|
||||||
|
title: "Relay limit",
|
||||||
|
description: "The maximum number of relays to publish zap receipts to"
|
||||||
|
) %>
|
||||||
|
</ul>
|
||||||
|
<% end %>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<th>Invited by</th>
|
<th>Invited by</th>
|
||||||
<td>
|
<td>
|
||||||
<% if @user.inviter %>
|
<% if @user.inviter %>
|
||||||
<%= link_to @user.inviter.address, admin_user_path(@user.inviter.address), class: 'ks-text-link' %>
|
<%= link_to @user.inviter.cn, admin_user_path(@user.inviter.cn), class: 'ks-text-link' %>
|
||||||
<% else %>—<% end %>
|
<% else %>—<% end %>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
<% if @user.invitees.length > 0 %>
|
<% if @user.invitees.length > 0 %>
|
||||||
<ul class="mb-0">
|
<ul class="mb-0">
|
||||||
<% @user.invitees.order(cn: :asc).each do |invitee| %>
|
<% @user.invitees.order(cn: :asc).each do |invitee| %>
|
||||||
<li class="leading-none mb-2 last:mb-0"><%= link_to invitee.address, admin_user_path(invitee.address), class: 'ks-text-link' %></li>
|
<li class="leading-none mb-2 last:mb-0"><%= link_to invitee.cn, admin_user_path(invitee.cn), class: 'ks-text-link' %></li>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
<% else %>—<% end %>
|
<% else %>—<% end %>
|
||||||
@@ -124,6 +124,19 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% if Setting.email_enabled %>
|
||||||
|
<tr>
|
||||||
|
<td>E-Mail</td>
|
||||||
|
<td>
|
||||||
|
<%= render FormElements::ToggleComponent.new(
|
||||||
|
enabled: Flipper.enabled?(:email, current_user),
|
||||||
|
input_enabled: false
|
||||||
|
) %>
|
||||||
|
</td>
|
||||||
|
<td class="text-right">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
<% if Setting.gitea_enabled %>
|
<% if Setting.gitea_enabled %>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Gitea</td>
|
<td>Gitea</td>
|
||||||
@@ -182,6 +195,33 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% if Setting.nostr_enabled %>
|
||||||
|
<tr>
|
||||||
|
<td>Nostr</td>
|
||||||
|
<td>
|
||||||
|
<%= render FormElements::ToggleComponent.new(
|
||||||
|
enabled: @user.nostr_pubkey.present?,
|
||||||
|
input_enabled: false
|
||||||
|
) %>
|
||||||
|
</td>
|
||||||
|
<td class="text-right">
|
||||||
|
<%= link_to "Open profile", "https://njump.me/#{@user.nostr_pubkey_bech32}", class: "btn-sm btn-gray" %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
<% if Setting.remotestorage_enabled %>
|
||||||
|
<tr>
|
||||||
|
<td>remoteStorage</td>
|
||||||
|
<td>
|
||||||
|
<%= render FormElements::ToggleComponent.new(
|
||||||
|
enabled: Flipper.enabled?(:remotestorage, current_user) && @services_enabled.include?("remotestorage"),
|
||||||
|
input_enabled: false
|
||||||
|
) %>
|
||||||
|
</td>
|
||||||
|
<td class="text-right">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
<p>
|
<p>
|
||||||
There's something to do for everyone, especially non-programmers! For
|
There's something to do for everyone, especially non-programmers! For
|
||||||
example, we need more help with graphics, UI/UX design, and
|
example, we need more help with graphics, UI/UX design, and
|
||||||
content/copywriting. We also need moderators for social media. And beta
|
content/copywriting. Also, testing any of our software and reporting
|
||||||
testers for our software. The list doesn't end there.
|
issues you encounter along the way is very valuable.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
A good way to get started is to join one of our
|
A good way to get started is to join one of our
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
We have run two 6-month trials so far, with the next trial period
|
We have run two 6-month trials so far, with the next trial period
|
||||||
starting sometime in Q1 2024. Watch your email for notifications about it!
|
starting sometime in Q2 2024. Watch your email for notifications about it!
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="services grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
|
<div class="services grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
|
||||||
<% if Setting.ejabberd_enabled? %>
|
<% if Setting.ejabberd_enabled? %>
|
||||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||||
bg-cover bg-[center_top_-50px] bg-no-repeat
|
bg-[length:86%] bg-[center_top_-40px] bg-no-repeat
|
||||||
bg-[url(/img/logos/icon_xmpp.svg)]">
|
bg-[url(/img/logos/icon_xmpp.svg)]">
|
||||||
<%= link_to services_chat_path,
|
<%= link_to services_chat_path,
|
||||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% if Setting.mastodon_enabled? %>
|
<% if Setting.mastodon_enabled? %>
|
||||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||||
bg-[length:80%] bg-[right_top_-30px] bg-no-repeat
|
bg-[length:88%] bg-[center_top_-40px] bg-no-repeat
|
||||||
bg-[url(/img/logos/icon_mastodon.svg)]">
|
bg-[url(/img/logos/icon_mastodon.svg)]">
|
||||||
<%= link_to services_mastodon_path, class: "block h-full px-6 py-6 rounded-md" do %>
|
<%= link_to services_mastodon_path, class: "block h-full px-6 py-6 rounded-md" do %>
|
||||||
<h3 class="mb-3.5">Mastodon</h3>
|
<h3 class="mb-3.5">Mastodon</h3>
|
||||||
@@ -30,7 +30,9 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% if Setting.email_enabled? &&
|
<% if Setting.email_enabled? &&
|
||||||
Flipper.enabled?(:email, current_user) %>
|
Flipper.enabled?(:email, current_user) %>
|
||||||
<div class="border border-gray-300 rounded-md hover:border-gray-400">
|
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||||
|
bg-[length:90%] bg-[center_top_-160px] bg-no-repeat
|
||||||
|
bg-[url(/img/logos/icon_mail.svg)]">
|
||||||
<%= link_to services_email_path, class: "block h-full px-6 py-6 rounded-md" do %>
|
<%= link_to services_email_path, class: "block h-full px-6 py-6 rounded-md" do %>
|
||||||
<h3 class="mb-3.5">E-Mail</h3>
|
<h3 class="mb-3.5">E-Mail</h3>
|
||||||
<p class="text-gray-600">
|
<p class="text-gray-600">
|
||||||
@@ -41,13 +43,13 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% if Setting.discourse_enabled? %>
|
<% if Setting.discourse_enabled? %>
|
||||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||||
bg-[length:95%] bg-center bg-no-repeat
|
bg-[length:80%] bg-center bg-no-repeat
|
||||||
bg-[url(/img/logos/icon_discourse.svg)]">
|
bg-[url(/img/logos/icon_discourse.svg)]">
|
||||||
<%= link_to "#{Setting.discourse_public_url}/session/sso?return_path=/",
|
<%= link_to "#{Setting.discourse_public_url}/session/sso?return_path=/",
|
||||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||||
<h3 class="mb-3.5">Discourse</h3>
|
<h3 class="mb-3.5">Discourse</h3>
|
||||||
<p class="text-gray-600">
|
<p class="text-gray-600">
|
||||||
Kosmos community forums and user support/help site
|
Community forums and support/help site
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,7 +69,9 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% if Setting.remotestorage_enabled? &&
|
<% if Setting.remotestorage_enabled? &&
|
||||||
Flipper.enabled?(:remotestorage, current_user) %>
|
Flipper.enabled?(:remotestorage, current_user) %>
|
||||||
<div class="border border-gray-300 rounded-md hover:border-gray-400">
|
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||||
|
bg-[length:80%] bg-[center_top_-156px] bg-no-repeat
|
||||||
|
bg-[url(/img/logos/icon_remotestorage.svg)]">
|
||||||
<%= link_to services_storage_path,
|
<%= link_to services_storage_path,
|
||||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||||
<h3 class="mb-3.5">Storage</h3>
|
<h3 class="mb-3.5">Storage</h3>
|
||||||
@@ -79,7 +83,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% if Setting.gitea_enabled? %>
|
<% if Setting.gitea_enabled? %>
|
||||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||||
bg-cover bg-center bg-no-repeat
|
bg-[length:92%] bg-center bg-no-repeat
|
||||||
bg-[url(/img/logos/icon_gitea.png)]">
|
bg-[url(/img/logos/icon_gitea.png)]">
|
||||||
<%= link_to Setting.gitea_public_url,
|
<%= link_to Setting.gitea_public_url,
|
||||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||||
@@ -92,7 +96,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% if Setting.droneci_enabled? %>
|
<% if Setting.droneci_enabled? %>
|
||||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||||
bg-cover bg-[center_top_-70px] bg-no-repeat
|
bg-[length:86%] bg-[center_top_-60px] bg-no-repeat
|
||||||
bg-[url(/img/logos/icon_droneci.svg)]">
|
bg-[url(/img/logos/icon_droneci.svg)]">
|
||||||
<%= link_to Setting.droneci_public_url,
|
<%= link_to Setting.droneci_public_url,
|
||||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||||
|
|||||||
@@ -55,4 +55,27 @@
|
|||||||
<%= f.submit "Log in", class: 'btn-md btn-blue w-full', tabindex: "4" %>
|
<%= f.submit "Log in", class: 'btn-md btn-blue w-full', tabindex: "4" %>
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<div data-controller="nostr-login"
|
||||||
|
data-nostr-login-target="loginForm"
|
||||||
|
data-nostr-login-site-value="<%= Setting.accounts_domain %>"
|
||||||
|
data-nostr-login-shared-secret-value="<%= session[:shared_secret] %>"
|
||||||
|
class="hidden">
|
||||||
|
<div class="relative my-6">
|
||||||
|
<div class="absolute inset-0 flex items-center" aria-hidden="true">
|
||||||
|
<div class="w-full border-t border-gray-200"></div>
|
||||||
|
</div>
|
||||||
|
<div class="relative flex justify-center">
|
||||||
|
<span class="bg-white px-2 text-sm text-gray-500 italic">or</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
<button disabled tabindex="5"
|
||||||
|
class="w-full btn-md btn-gray text-purple-600"
|
||||||
|
data-nostr-login-target="loginButton"
|
||||||
|
data-action="nostr-login#login">
|
||||||
|
Log in with Nostr
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -100,6 +100,14 @@
|
|||||||
["Website", "https://www.thunderbird.net"]
|
["Website", "https://www.thunderbird.net"]
|
||||||
]
|
]
|
||||||
) %>
|
) %>
|
||||||
|
<%= render AppInfoComponent.new(
|
||||||
|
name: "Geary",
|
||||||
|
description: "Built around conversations, for the GNOME desktop",
|
||||||
|
icon_path: "/img/logos/icon_geary.png",
|
||||||
|
links: [
|
||||||
|
["Website", "https://wiki.gnome.org/Apps/Geary"]
|
||||||
|
]
|
||||||
|
) %>
|
||||||
</div>
|
</div>
|
||||||
<div id="apps-windows" class="hidden grid grid-cols-1 gap-6"
|
<div id="apps-windows" class="hidden grid grid-cols-1 gap-6"
|
||||||
data-tabs-target="panel">
|
data-tabs-target="panel">
|
||||||
|
|||||||
@@ -98,7 +98,17 @@
|
|||||||
description: "The official Web app",
|
description: "The official Web app",
|
||||||
icon_path: "/img/logos/icon_mastodon-2.svg",
|
icon_path: "/img/logos/icon_mastodon-2.svg",
|
||||||
links: [
|
links: [
|
||||||
["Launch", "https://kosmos.social"]
|
["Launch", "https://kosmos.social"],
|
||||||
|
["GitHub", "https://github.com/mastodon/mastodon"]
|
||||||
|
]
|
||||||
|
) %>
|
||||||
|
<%= render AppInfoComponent.new(
|
||||||
|
name: "Phanpy",
|
||||||
|
description: " A slick, feature-rich Web app for mobile and desktop",
|
||||||
|
icon_path: "/img/logos/icon_phanpy.svg",
|
||||||
|
links: [
|
||||||
|
["Launch", "https://phanpy.social"],
|
||||||
|
["GitHub", "https://github.com/cheeaun/phanpy"]
|
||||||
]
|
]
|
||||||
) %>
|
) %>
|
||||||
<%= render AppInfoComponent.new(
|
<%= render AppInfoComponent.new(
|
||||||
@@ -150,6 +160,15 @@
|
|||||||
["Google Play", "https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk"]
|
["Google Play", "https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk"]
|
||||||
]
|
]
|
||||||
) %>
|
) %>
|
||||||
|
<%= render AppInfoComponent.new(
|
||||||
|
name: "Phanpy",
|
||||||
|
description: " A slick, feature-rich Web app for mobile and desktop",
|
||||||
|
icon_path: "/img/logos/icon_phanpy.svg",
|
||||||
|
links: [
|
||||||
|
["Launch", "https://phanpy.social"],
|
||||||
|
["GitHub", "https://github.com/cheeaun/phanpy"]
|
||||||
|
]
|
||||||
|
) %>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
|
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
|
||||||
<%= render AppInfoComponent.new(
|
<%= render AppInfoComponent.new(
|
||||||
@@ -180,6 +199,15 @@
|
|||||||
["App Store", "https://apps.apple.com/app/mammoth-for-mastodon/id1667573899"]
|
["App Store", "https://apps.apple.com/app/mammoth-for-mastodon/id1667573899"]
|
||||||
]
|
]
|
||||||
) %>
|
) %>
|
||||||
|
<%= render AppInfoComponent.new(
|
||||||
|
name: "Phanpy",
|
||||||
|
description: " A slick, feature-rich Web app for mobile and desktop",
|
||||||
|
icon_path: "/img/logos/icon_phanpy.svg",
|
||||||
|
links: [
|
||||||
|
["Launch", "https://phanpy.social"],
|
||||||
|
["GitHub", "https://github.com/cheeaun/phanpy"]
|
||||||
|
]
|
||||||
|
) %>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
|
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
|
||||||
<%= render AppInfoComponent.new(
|
<%= render AppInfoComponent.new(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<%= render FormElements::FieldsetComponent.new(
|
<%= render FormElements::FieldsetComponent.new(
|
||||||
positioning: :horizontal,
|
positioning: :horizontal,
|
||||||
title: "Sats received",
|
title: "Sats received",
|
||||||
description: "Notify me when sats are sent to my Lightning Address"
|
description: "Notify me when sats are sent to my Lightning account"
|
||||||
) do %>
|
) do %>
|
||||||
<% f.fields_for :preferences do |p| %>
|
<% f.fields_for :preferences do |p| %>
|
||||||
<%= p.select :lightning_notify_sats_received, options_for_select([
|
<%= p.select :lightning_notify_sats_received, options_for_select([
|
||||||
@@ -15,6 +15,38 @@
|
|||||||
], selected: @user.preferences[:lightning_notify_sats_received]) %>
|
], selected: @user.preferences[:lightning_notify_sats_received]) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% if @user.nostr_pubkey.present? %>
|
||||||
|
<%= render FormElements::FieldsetComponent.new(
|
||||||
|
positioning: :horizontal,
|
||||||
|
title: "Zap received",
|
||||||
|
description: "Notify me when someone zaps me on Nostr"
|
||||||
|
) do %>
|
||||||
|
<% f.fields_for :preferences do |p| %>
|
||||||
|
<%= p.select :lightning_notify_zap_received, options_for_select([
|
||||||
|
["off", "disabled"],
|
||||||
|
["Chat (Jabber)", "xmpp"],
|
||||||
|
["E-Mail", "email"]
|
||||||
|
], selected: @user.preferences[:lightning_notify_zap_received]) %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% if @notifications_enabled %>
|
||||||
|
<%= render FormElements::FieldsetToggleComponent.new(
|
||||||
|
field_name: "user[preferences][lightning_notify_only_with_message]",
|
||||||
|
enabled: @user.preferences[:lightning_notify_only_with_message],
|
||||||
|
title: "Ignore transactions without message",
|
||||||
|
description: "Only send notifications when there is a message attached to the payment"
|
||||||
|
) %>
|
||||||
|
<%= render FormElements::FieldsetComponent.new(
|
||||||
|
title: "Minimum amount",
|
||||||
|
description: "Only send notifications when amount is higher than this"
|
||||||
|
) do %>
|
||||||
|
<%= f.number_field :lightning_notify_min_sats,
|
||||||
|
name: "user[preferences][lightning_notify_min_sats]",
|
||||||
|
class: "w-full",
|
||||||
|
value: @user.preferences[:lightning_notify_min_sats].to_i %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<h4 class="mb-0">Public Key</h4>
|
<h4 class="mb-0">Public Key</h4>
|
||||||
<div data-controller="settings--nostr-pubkey"
|
<div data-controller="settings--nostr-pubkey"
|
||||||
data-settings--nostr-pubkey-user-address-value="<%= current_user.address %>"
|
data-settings--nostr-pubkey-user-address-value="<%= current_user.address %>"
|
||||||
|
data-settings--nostr-pubkey-site-value="<%= Setting.accounts_domain %>"
|
||||||
data-settings--nostr-pubkey-shared-secret-value="<%= session[:shared_secret] %>"
|
data-settings--nostr-pubkey-shared-secret-value="<%= session[:shared_secret] %>"
|
||||||
data-settings--nostr-pubkey-pubkey-hex-value="<%= current_user.nostr_pubkey %>">
|
data-settings--nostr-pubkey-pubkey-hex-value="<%= current_user.nostr_pubkey %>">
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
lightning_notify_sats_received: disabled # or xmpp, email
|
lightning_notify_sats_received: email
|
||||||
remotestorage_notify_auth_created: email # or xmpp, email
|
lightning_notify_zap_received: disabled
|
||||||
|
lightning_notify_min_sats: 0
|
||||||
|
lightning_notify_only_with_message: false
|
||||||
|
remotestorage_notify_auth_created: email
|
||||||
xmpp_exchange_contacts_with_invitees: true
|
xmpp_exchange_contacts_with_invitees: true
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
require 'sidekiq/web'
|
require 'sidekiq/web'
|
||||||
|
|
||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
devise_for :users, controllers: { confirmations: 'users/confirmations' }
|
devise_for :users, controllers: {
|
||||||
|
confirmations: 'users/confirmations',
|
||||||
|
sessions: 'users/sessions'
|
||||||
|
}
|
||||||
|
|
||||||
|
devise_scope :user do
|
||||||
|
post 'users/nostr_login', to: 'users/sessions#nostr_login'
|
||||||
|
end
|
||||||
|
|
||||||
get 'welcome', to: 'welcome#index'
|
get 'welcome', to: 'welcome#index'
|
||||||
get 'check_your_email', to: 'welcome#check_your_email'
|
get 'check_your_email', to: 'welcome#check_your_email'
|
||||||
|
|||||||
@@ -2,3 +2,5 @@
|
|||||||
:queues:
|
:queues:
|
||||||
- default
|
- default
|
||||||
- mailers
|
- mailers
|
||||||
|
- remotestorage
|
||||||
|
- nostr
|
||||||
|
|||||||
13
db/migrate/20240422171653_create_zaps.rb
Normal file
13
db/migrate/20240422171653_create_zaps.rb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
class CreateZaps < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
create_table :zaps do |t|
|
||||||
|
t.references :user, null: false, foreign_key: true
|
||||||
|
t.json :request
|
||||||
|
t.json :receipt, default: nil
|
||||||
|
t.text :payment_request
|
||||||
|
t.bigint :amount
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
9
db/migrate/20240607123654_add_settled_at_to_zaps.rb
Normal file
9
db/migrate/20240607123654_add_settled_at_to_zaps.rb
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
class AddSettledAtToZaps < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
add_column :zaps, :settled_at, :datetime, default: nil
|
||||||
|
|
||||||
|
Zap.where.not(receipt: nil).each do |zap|
|
||||||
|
zap.update! settled_at: Time.at(zap.receipt_event.created_at).to_datetime
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
15
db/schema.rb
15
db/schema.rb
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.1].define(version: 2024_03_16_153558) do
|
ActiveRecord::Schema[7.1].define(version: 2024_06_07_123654) do
|
||||||
create_table "active_storage_attachments", force: :cascade do |t|
|
create_table "active_storage_attachments", force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.string "record_type", null: false
|
t.string "record_type", null: false
|
||||||
@@ -136,8 +136,21 @@ ActiveRecord::Schema[7.1].define(version: 2024_03_16_153558) do
|
|||||||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "zaps", force: :cascade do |t|
|
||||||
|
t.integer "user_id", null: false
|
||||||
|
t.json "request"
|
||||||
|
t.json "receipt"
|
||||||
|
t.text "payment_request"
|
||||||
|
t.bigint "amount"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.datetime "settled_at"
|
||||||
|
t.index ["user_id"], name: "index_zaps_on_user_id"
|
||||||
|
end
|
||||||
|
|
||||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
||||||
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
||||||
add_foreign_key "remote_storage_authorizations", "app_catalog_web_apps", column: "web_app_id"
|
add_foreign_key "remote_storage_authorizations", "app_catalog_web_apps", column: "web_app_id"
|
||||||
add_foreign_key "remote_storage_authorizations", "users"
|
add_foreign_key "remote_storage_authorizations", "users"
|
||||||
|
add_foreign_key "zaps", "users"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ services:
|
|||||||
RS_REDIS_URL: redis://redis:6379/1
|
RS_REDIS_URL: redis://redis:6379/1
|
||||||
RS_STORAGE_URL: "http://localhost:4567"
|
RS_STORAGE_URL: "http://localhost:4567"
|
||||||
S3_ENABLED: false
|
S3_ENABLED: false
|
||||||
|
NOSTR_PUBLIC_KEY: bdd76ce2934b2f591f9fad2ebe9da18f20d2921de527494ba00eeaa0a0efadcf
|
||||||
|
NOSTR_PRIVATE_KEY: 7c3ef7e448505f0615137af38569d01807d3b05b5005d5ecf8aaafcd40323cea
|
||||||
|
NOSTR_RELAY_URL: "ws://strfry:7777"
|
||||||
depends_on:
|
depends_on:
|
||||||
- ldap
|
- ldap
|
||||||
- redis
|
- redis
|
||||||
@@ -107,6 +110,24 @@ services:
|
|||||||
- minio
|
- minio
|
||||||
- redis
|
- redis
|
||||||
|
|
||||||
|
strfry:
|
||||||
|
image: gitea.kosmos.org/kosmos/strfry-deno:1.1.1
|
||||||
|
volumes:
|
||||||
|
- ./docker/strfry/strfry.conf:/etc/strfry.conf
|
||||||
|
- ./extras/strfry:/opt/strfry
|
||||||
|
- strfry-data:/var/lib/strfry
|
||||||
|
networks:
|
||||||
|
- external_network
|
||||||
|
- internal_network
|
||||||
|
ports:
|
||||||
|
- "4777:7777"
|
||||||
|
environment:
|
||||||
|
LDAP_URL: 'ldap://ldap:3389'
|
||||||
|
LDAP_BIND_DN: 'cn=Directory Manager'
|
||||||
|
LDAP_PASSWORD: passthebutter
|
||||||
|
LDAP_SEARCH_DN: 'ou=kosmos.org,cn=users,dc=kosmos,dc=org'
|
||||||
|
WHITELIST_PUBKEYS: 'bdd76ce2934b2f591f9fad2ebe9da18f20d2921de527494ba00eeaa0a0efadcf'
|
||||||
|
|
||||||
# phpldapadmin:
|
# phpldapadmin:
|
||||||
# image: osixia/phpldapadmin:0.9.0
|
# image: osixia/phpldapadmin:0.9.0
|
||||||
# ports:
|
# ports:
|
||||||
@@ -128,3 +149,5 @@ volumes:
|
|||||||
driver: local
|
driver: local
|
||||||
redis-data:
|
redis-data:
|
||||||
driver: local
|
driver: local
|
||||||
|
strfry-data:
|
||||||
|
driver: local
|
||||||
|
|||||||
138
docker/strfry/strfry.conf
Normal file
138
docker/strfry/strfry.conf
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
##
|
||||||
|
## Default strfry config
|
||||||
|
##
|
||||||
|
|
||||||
|
# Directory that contains the strfry LMDB database (restart required)
|
||||||
|
db = "/var/lib/strfry/"
|
||||||
|
|
||||||
|
dbParams {
|
||||||
|
# Maximum number of threads/processes that can simultaneously have LMDB transactions open (restart required)
|
||||||
|
maxreaders = 256
|
||||||
|
|
||||||
|
# Size of mmap() to use when loading LMDB (default is 10TB, does *not* correspond to disk-space used) (restart required)
|
||||||
|
mapsize = 10995116277760
|
||||||
|
|
||||||
|
# Disables read-ahead when accessing the LMDB mapping. Reduces IO activity when DB size is larger than RAM. (restart required)
|
||||||
|
noReadAhead = false
|
||||||
|
}
|
||||||
|
|
||||||
|
events {
|
||||||
|
# Maximum size of normalised JSON, in bytes
|
||||||
|
maxEventSize = 65536
|
||||||
|
|
||||||
|
# Events newer than this will be rejected
|
||||||
|
rejectEventsNewerThanSeconds = 900
|
||||||
|
|
||||||
|
# Events older than this will be rejected
|
||||||
|
rejectEventsOlderThanSeconds = 94608000
|
||||||
|
|
||||||
|
# Ephemeral events older than this will be rejected
|
||||||
|
rejectEphemeralEventsOlderThanSeconds = 60
|
||||||
|
|
||||||
|
# Ephemeral events will be deleted from the DB when older than this
|
||||||
|
ephemeralEventsLifetimeSeconds = 300
|
||||||
|
|
||||||
|
# Maximum number of tags allowed
|
||||||
|
maxNumTags = 2000
|
||||||
|
|
||||||
|
# Maximum size for tag values, in bytes
|
||||||
|
maxTagValSize = 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
relay {
|
||||||
|
# Interface to listen on. Use 0.0.0.0 to listen on all interfaces (restart required)
|
||||||
|
bind = "0.0.0.0"
|
||||||
|
|
||||||
|
# Port to open for the nostr websocket protocol (restart required)
|
||||||
|
port = 7777
|
||||||
|
|
||||||
|
# Set OS-limit on maximum number of open files/sockets (if 0, don't attempt to set) (restart required)
|
||||||
|
nofiles = 200000
|
||||||
|
|
||||||
|
# HTTP header that contains the client's real IP, before reverse proxying (ie x-real-ip) (MUST be all lower-case)
|
||||||
|
realIpHeader = ""
|
||||||
|
|
||||||
|
info {
|
||||||
|
# NIP-11: Name of this server. Short/descriptive (< 30 characters)
|
||||||
|
name = "Akkounts Nostr Relay"
|
||||||
|
|
||||||
|
# NIP-11: Detailed information about relay, free-form
|
||||||
|
description = "Local strfry instance for akkounts development"
|
||||||
|
|
||||||
|
# NIP-11: Administrative nostr pubkey, for contact purposes
|
||||||
|
pubkey = ""
|
||||||
|
|
||||||
|
# NIP-11: Alternative administrative contact (email, website, etc)
|
||||||
|
contact = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Maximum accepted incoming websocket frame size (should be larger than max event) (restart required)
|
||||||
|
maxWebsocketPayloadSize = 131072
|
||||||
|
|
||||||
|
# Websocket-level PING message frequency (should be less than any reverse proxy idle timeouts) (restart required)
|
||||||
|
autoPingSeconds = 55
|
||||||
|
|
||||||
|
# If TCP keep-alive should be enabled (detect dropped connections to upstream reverse proxy)
|
||||||
|
enableTcpKeepalive = false
|
||||||
|
|
||||||
|
# How much uninterrupted CPU time a REQ query should get during its DB scan
|
||||||
|
queryTimesliceBudgetMicroseconds = 10000
|
||||||
|
|
||||||
|
# Maximum records that can be returned per filter
|
||||||
|
maxFilterLimit = 500
|
||||||
|
|
||||||
|
# Maximum number of subscriptions (concurrent REQs) a connection can have open at any time
|
||||||
|
maxSubsPerConnection = 20
|
||||||
|
|
||||||
|
writePolicy {
|
||||||
|
# If non-empty, path to an executable script that implements the writePolicy plugin logic
|
||||||
|
plugin = "/opt/strfry/strfry-policy.ts"
|
||||||
|
}
|
||||||
|
|
||||||
|
compression {
|
||||||
|
# Use permessage-deflate compression if supported by client. Reduces bandwidth, but slight increase in CPU (restart required)
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
# Maintain a sliding window buffer for each connection. Improves compression, but uses more memory (restart required)
|
||||||
|
slidingWindow = true
|
||||||
|
}
|
||||||
|
|
||||||
|
logging {
|
||||||
|
# Dump all incoming messages
|
||||||
|
dumpInAll = true
|
||||||
|
|
||||||
|
# Dump all incoming EVENT messages
|
||||||
|
dumpInEvents = false
|
||||||
|
|
||||||
|
# Dump all incoming REQ/CLOSE messages
|
||||||
|
dumpInReqs = false
|
||||||
|
|
||||||
|
# Log performance metrics for initial REQ database scans
|
||||||
|
dbScanPerf = true
|
||||||
|
|
||||||
|
# Log reason for invalid event rejection? Can be disabled to silence excessive logging
|
||||||
|
invalidEvents = true
|
||||||
|
}
|
||||||
|
|
||||||
|
numThreads {
|
||||||
|
# Ingester threads: route incoming requests, validate events/sigs (restart required)
|
||||||
|
ingester = 3
|
||||||
|
|
||||||
|
# reqWorker threads: Handle initial DB scan for events (restart required)
|
||||||
|
reqWorker = 3
|
||||||
|
|
||||||
|
# reqMonitor threads: Handle filtering of new events (restart required)
|
||||||
|
reqMonitor = 3
|
||||||
|
|
||||||
|
# negentropy threads: Handle negentropy protocol messages (restart required)
|
||||||
|
negentropy = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
negentropy {
|
||||||
|
# Support negentropy protocol messages
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
# Maximum records that sync will process before returning an error
|
||||||
|
maxSyncEvents = 1000000
|
||||||
|
}
|
||||||
|
}
|
||||||
5
extras/strfry/deno.json
Normal file
5
extras/strfry/deno.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"@nostr/tools": "jsr:@nostr/tools@^2.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
196
extras/strfry/deno.lock
generated
Normal file
196
extras/strfry/deno.lock
generated
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
{
|
||||||
|
"version": "3",
|
||||||
|
"packages": {
|
||||||
|
"specifiers": {
|
||||||
|
"jsr:@nostr/tools@^2.3.1": "jsr:@nostr/tools@2.3.1",
|
||||||
|
"npm:@noble/ciphers@^0.5.1": "npm:@noble/ciphers@0.5.3",
|
||||||
|
"npm:@noble/curves@1.2.0": "npm:@noble/curves@1.2.0",
|
||||||
|
"npm:@noble/hashes@1.3.1": "npm:@noble/hashes@1.3.1",
|
||||||
|
"npm:@scure/base@1.1.1": "npm:@scure/base@1.1.1",
|
||||||
|
"npm:ldapts": "npm:ldapts@7.0.12"
|
||||||
|
},
|
||||||
|
"jsr": {
|
||||||
|
"@nostr/tools@2.3.1": {
|
||||||
|
"integrity": "af01dc45cb28784c584d7a0699707196f397bcc53946efa582a01b11ddde4d61",
|
||||||
|
"dependencies": [
|
||||||
|
"npm:@noble/ciphers@^0.5.1",
|
||||||
|
"npm:@noble/curves@1.2.0",
|
||||||
|
"npm:@noble/hashes@1.3.1",
|
||||||
|
"npm:@scure/base@1.1.1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"npm": {
|
||||||
|
"@noble/ciphers@0.5.3": {
|
||||||
|
"integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"@noble/curves@1.2.0": {
|
||||||
|
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "@noble/hashes@1.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@noble/hashes@1.3.1": {
|
||||||
|
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"@noble/hashes@1.3.2": {
|
||||||
|
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"@scure/base@1.1.1": {
|
||||||
|
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"@types/asn1@0.2.4": {
|
||||||
|
"integrity": "sha512-V91DSJ2l0h0gRhVP4oBfBzRBN9lAbPUkGDMCnwedqPKX2d84aAMc9CulOvxdw1f7DfEYx99afab+Rsm3e52jhA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "@types/node@18.16.19"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/node@18.16.19": {
|
||||||
|
"integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"@types/uuid@9.0.8": {
|
||||||
|
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"asn1@0.2.6": {
|
||||||
|
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": "safer-buffer@2.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"debug@4.3.5": {
|
||||||
|
"integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "ms@2.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ldapts@7.0.12": {
|
||||||
|
"integrity": "sha512-orwgIejUi/ZyGah9y8jWZmFUg8Ci5M8WAv0oZjSf3MVuk1sRBdor9Qy1ttGHbYpWj96HXKFunQ8AYZ8WWGp17g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/asn1": "@types/asn1@0.2.4",
|
||||||
|
"@types/uuid": "@types/uuid@9.0.8",
|
||||||
|
"asn1": "asn1@0.2.6",
|
||||||
|
"debug": "debug@4.3.5",
|
||||||
|
"strict-event-emitter-types": "strict-event-emitter-types@2.0.0",
|
||||||
|
"uuid": "uuid@9.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ms@2.1.2": {
|
||||||
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"safer-buffer@2.1.2": {
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"strict-event-emitter-types@2.0.0": {
|
||||||
|
"integrity": "sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"uuid@9.0.1": {
|
||||||
|
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||||
|
"dependencies": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"remote": {
|
||||||
|
"https://deno.land/std@0.181.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462",
|
||||||
|
"https://deno.land/std@0.181.0/bytes/bytes_list.ts": "b4cbdfd2c263a13e8a904b12d082f6177ea97d9297274a4be134e989450dfa6a",
|
||||||
|
"https://deno.land/std@0.181.0/bytes/concat.ts": "d26d6f3d7922e6d663dacfcd357563b7bf4a380ce5b9c2bbe0c8586662f25ce2",
|
||||||
|
"https://deno.land/std@0.181.0/bytes/copy.ts": "939d89e302a9761dcf1d9c937c7711174ed74c59eef40a1e4569a05c9de88219",
|
||||||
|
"https://deno.land/std@0.181.0/fmt/colors.ts": "d67e3cd9f472535241a8e410d33423980bec45047e343577554d3356e1f0ef4e",
|
||||||
|
"https://deno.land/std@0.181.0/io/buf_reader.ts": "abeb92b18426f11d72b112518293a96aef2e6e55f80b84235e8971ac910affb5",
|
||||||
|
"https://deno.land/std@0.181.0/io/buf_writer.ts": "48c33c8f00b61dcbc7958706741cec8e59810bd307bc6a326cbd474fe8346dfd",
|
||||||
|
"https://deno.land/std@0.181.0/io/buffer.ts": "17f4410eaaa60a8a85733e8891349a619eadfbbe42e2f319283ce2b8f29723ab",
|
||||||
|
"https://deno.land/std@0.181.0/io/copy_n.ts": "0cc7ce07c75130f6fc18621ec1911c36e147eb9570664fee0ea12b1988167590",
|
||||||
|
"https://deno.land/std@0.181.0/io/limited_reader.ts": "6c9a216f8eef39c1ee2a6b37a29372c8fc63455b2eeb91f06d9646f8f759fc8b",
|
||||||
|
"https://deno.land/std@0.181.0/io/mod.ts": "2665bcccc1fd6e8627cca167c3e92aaecbd9897556b6f69e6d258070ef63fd9b",
|
||||||
|
"https://deno.land/std@0.181.0/io/multi_reader.ts": "9c2a0a31686c44b277e16da1d97b4686a986edcee48409b84be25eedbc39b271",
|
||||||
|
"https://deno.land/std@0.181.0/io/read_delim.ts": "c02b93cc546ae8caad8682ae270863e7ace6daec24c1eddd6faabc95a9d876a3",
|
||||||
|
"https://deno.land/std@0.181.0/io/read_int.ts": "7cb8bcdfaf1107586c3bacc583d11c64c060196cb070bb13ae8c2061404f911f",
|
||||||
|
"https://deno.land/std@0.181.0/io/read_lines.ts": "c526c12a20a9386dc910d500f9cdea43cba974e853397790bd146817a7eef8cc",
|
||||||
|
"https://deno.land/std@0.181.0/io/read_long.ts": "f0aaa420e3da1261c5d33c5e729f09922f3d9fa49f046258d4ff7a00d800c71e",
|
||||||
|
"https://deno.land/std@0.181.0/io/read_range.ts": "28152daf32e43dd9f7d41d8466852b0d18ad766cd5c4334c91fef6e1b3a74eb5",
|
||||||
|
"https://deno.land/std@0.181.0/io/read_short.ts": "805cb329574b850b84bf14a92c052c59b5977a492cd780c41df8ad40826c1a20",
|
||||||
|
"https://deno.land/std@0.181.0/io/read_string_delim.ts": "5dc9f53bdf78e7d4ee1e56b9b60352238ab236a71c3e3b2a713c3d78472a53ce",
|
||||||
|
"https://deno.land/std@0.181.0/io/slice_long_to_bytes.ts": "48d9bace92684e880e46aa4a2520fc3867f9d7ce212055f76ecc11b22f9644b7",
|
||||||
|
"https://deno.land/std@0.181.0/io/string_reader.ts": "da0f68251b3d5b5112485dfd4d1b1936135c9b4d921182a7edaf47f74c25cc8f",
|
||||||
|
"https://deno.land/std@0.181.0/io/string_writer.ts": "8a03c5858c24965a54c6538bed15f32a7c72f5704a12bda56f83a40e28e5433e",
|
||||||
|
"https://deno.land/std@0.181.0/testing/_diff.ts": "1a3c044aedf77647d6cac86b798c6417603361b66b54c53331b312caeb447aea",
|
||||||
|
"https://deno.land/std@0.181.0/testing/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7",
|
||||||
|
"https://deno.land/std@0.181.0/testing/asserts.ts": "e16d98b4d73ffc4ed498d717307a12500ae4f2cbe668f1a215632d19fcffc22f",
|
||||||
|
"https://deno.land/std@0.224.0/dotenv/mod.ts": "0180eaeedaaf88647318811cdaa418cc64dc51fb08354f91f5f480d0a1309f7d",
|
||||||
|
"https://deno.land/std@0.224.0/dotenv/parse.ts": "09977ff88dfd1f24f9973a338f0f91bbdb9307eb5ff6085446e7c423e4c7ba0c",
|
||||||
|
"https://deno.land/std@0.224.0/dotenv/stringify.ts": "275da322c409170160440836342eaa7cf012a1d11a7e700d8ca4e7f2f8aa4615",
|
||||||
|
"https://deno.land/std@0.88.0/async/deferred.ts": "f89ed49ba5e1dd0227c6bd5b23f017be46c3f92e4f0338dda08ff5aa54b9f6c9",
|
||||||
|
"https://deno.land/std@0.88.0/async/delay.ts": "9de1d8d07d1927767ab7f82434b883f3d8294fb19cad819691a2ad81a728cf3d",
|
||||||
|
"https://deno.land/std@0.88.0/async/mod.ts": "253b41c658d768613eacfb11caa0a9ca7148442f932018a45576f7f27554c853",
|
||||||
|
"https://deno.land/std@0.88.0/async/mux_async_iterator.ts": "b9091909db04cdb0af6f7807677372f64c1488de6c4bd86004511b064bf230d6",
|
||||||
|
"https://deno.land/std@0.88.0/async/pool.ts": "876f9e6815366cd017a3b4fbb9e9ae40310b1b6972f1bd541c94358bc11fb7e5",
|
||||||
|
"https://deno.land/std@0.88.0/encoding/base64.ts": "eecae390f1f1d1cae6f6c6d732ede5276bf4b9cd29b1d281678c054dc5cc009e",
|
||||||
|
"https://deno.land/std@0.88.0/encoding/hex.ts": "f952e0727bddb3b2fd2e6889d104eacbd62e92091f540ebd6459317a61932d9b",
|
||||||
|
"https://deno.land/std@0.88.0/fmt/colors.ts": "db22b314a2ae9430ae7460ce005e0a7130e23ae1c999157e3bb77cf55800f7e4",
|
||||||
|
"https://deno.land/std@0.88.0/node/_utils.ts": "067c386d676432e9418808851e8de72df7774f009a652904f62358b4c94504cf",
|
||||||
|
"https://deno.land/std@0.88.0/node/buffer.ts": "e98af24a3210d8fc3f022b6eb26d6e5bdf98fb0e02931e5983d20db9fed1b590",
|
||||||
|
"https://deno.land/std@0.88.0/testing/_diff.ts": "961eaf6d9f5b0a8556c9d835bbc6fa74f5addd7d3b02728ba7936ff93364f7a3",
|
||||||
|
"https://deno.land/std@0.88.0/testing/asserts.ts": "7fae8128125106ddf8e4b3ac84cc3b5fb2378e3fbf8ba38947ebe24faa002ce2",
|
||||||
|
"https://deno.land/x/module_cache@0.0.3/mod.ts": "c5e724477146e68b7a4d7ba440cd18f2ef4b28e4244ce48358c79efe98e3cd24",
|
||||||
|
"https://deno.land/x/sqlite@v3.7.1/build/sqlite.js": "c59f109f100c2bae0b9342f04e0d400583e2e3211d08bb71095177a4109ee5bf",
|
||||||
|
"https://deno.land/x/sqlite@v3.7.1/build/vfs.js": "08533cc78fb29b9d9bd62f6bb93e5ef333407013fed185776808f11223ba0e70",
|
||||||
|
"https://deno.land/x/sqlite@v3.7.1/mod.ts": "e09fc79d8065fe222578114b109b1fd60077bff1bb75448532077f784f4d6a83",
|
||||||
|
"https://deno.land/x/sqlite@v3.7.1/src/constants.ts": "90f3be047ec0a89bcb5d6fc30db121685fc82cb00b1c476124ff47a4b0472aa9",
|
||||||
|
"https://deno.land/x/sqlite@v3.7.1/src/db.ts": "59c6c2b5c4127132558bb8c610eadd811822f1a5d7f9c509704179ca192f94e0",
|
||||||
|
"https://deno.land/x/sqlite@v3.7.1/src/error.ts": "f7a15cb00d7c3797da1aefee3cf86d23e0ae92e73f0ba3165496c3816ab9503a",
|
||||||
|
"https://deno.land/x/sqlite@v3.7.1/src/function.ts": "e4c83b8ec64bf88bafad2407376b0c6a3b54e777593c70336fb40d43a79865f2",
|
||||||
|
"https://deno.land/x/sqlite@v3.7.1/src/query.ts": "d58abda928f6582d77bad685ecf551b1be8a15e8e38403e293ec38522e030cad",
|
||||||
|
"https://deno.land/x/sqlite@v3.7.1/src/wasm.ts": "e79d0baa6e42423257fb3c7cc98091c54399254867e0f34a09b5bdef37bd9487",
|
||||||
|
"https://esm.sh/nostr-tools@1.8.4?pin=v115": "62e5b620dbbaea0ee399efcc700260da12836a353fa521d35969d3454e591a77",
|
||||||
|
"https://esm.sh/v115/@noble/hashes@1.2.0/denonext/_assert.js": "2d47b1ae1c443fbcda3aa75e6d66c26da566d1775dcd757165314e8e9d1162da",
|
||||||
|
"https://esm.sh/v115/@noble/hashes@1.2.0/denonext/crypto.js": "0880be2fb91177484b9a5916a286aadce6a1c8b1b5cf6be47393361e6b121a17",
|
||||||
|
"https://esm.sh/v115/@noble/hashes@1.2.0/denonext/hmac.js": "cdb442a8326674449570b98daa44b07317908eae81205c178cab542ea754b91d",
|
||||||
|
"https://esm.sh/v115/@noble/hashes@1.2.0/denonext/pbkdf2.js": "e8b8e2ff70ecb35442fabfece10e76850ac8dc6aaf44a769871c9e6dbe60d264",
|
||||||
|
"https://esm.sh/v115/@noble/hashes@1.2.0/denonext/ripemd160.js": "8cd5e59afc12f6f6a2c980495f699a76d812ca30772d4c085ff8477fe4b1a2fe",
|
||||||
|
"https://esm.sh/v115/@noble/hashes@1.2.0/denonext/sha256.js": "8dec7d1bb4d0799f9cdf8f9ea7d8c3e91790255d547defcf62a626a0a190185e",
|
||||||
|
"https://esm.sh/v115/@noble/hashes@1.2.0/denonext/sha512.js": "85ccf57544faca95a6aeab11951f98f49e56b3cbad0618f624838c7e8fb4361d",
|
||||||
|
"https://esm.sh/v115/@noble/hashes@1.2.0/denonext/utils.js": "11431fc23031cb324977bc992e699fda8ec7c63fcc17c2b4f71a3902d48e99e5",
|
||||||
|
"https://esm.sh/v115/@noble/secp256k1@1.7.0/denonext/secp256k1.mjs": "36fb68b95b2f62de23d275be52b2eec68813083b93b78f7032492188ef59c77b",
|
||||||
|
"https://esm.sh/v115/@noble/secp256k1@1.7.1/denonext/secp256k1.mjs": "43c5a7ba14ae81b36e5ce64abf45962119527e926cddb764b7e510869b05f0bd",
|
||||||
|
"https://esm.sh/v115/@scure/base@1.1.1/denonext/base.mjs": "8f9cb853c4f6a4367c2f5bfb921d54b4ed61e41829944435e5878781b54d94a9",
|
||||||
|
"https://esm.sh/v115/@scure/bip32@1.1.4/denonext/bip32.mjs": "05471356192b1286874be6c28bea4ebac6dd6bc680bce795640604bb317c2165",
|
||||||
|
"https://esm.sh/v115/@scure/bip39@1.1.1/denonext/bip39.mjs": "00ccac2e221996db35b6780b3ae2cf37a153111bd1d348c9defe3a4341ec683d",
|
||||||
|
"https://esm.sh/v115/@scure/bip39@1.1.1/denonext/wordlists/english.js": "72ca7f3b2e856a62caa00441579008da89ea21a9c8a428ae547cdcffd17ae40c",
|
||||||
|
"https://esm.sh/v115/nostr-tools@1.8.4/denonext/nostr-tools.mjs": "f8023312404e4a83f0c052653643bcdbf5169a1585bd5399f11c65f37f7bcf16",
|
||||||
|
"https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/mod.ts": "26add79f9bf2b12d088bacd3417dbb590684171f80be2dbf2e6b83b324df54f7",
|
||||||
|
"https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/src/deps.ts": "3c06f4dafe1b04c2413977e9dfdc4956136505f401e0ced14a1c7aff484ad699",
|
||||||
|
"https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/src/io.ts": "1f87789a4ea53ed73438c475bb4b6a82eba2bb389d4c8c9179450a4b490f1953",
|
||||||
|
"https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/src/pipeline.ts": "4b881ebc1893b4f9f8dcbab260097a0402e0a398b937ef6723915db7c2a86a90",
|
||||||
|
"https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/src/policies/anti-duplication-policy.ts": "82a3868b671e68e1379104c0ee1fb8085a5c2d9b802b6eedf31eaae87e778a53",
|
||||||
|
"https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/src/policies/filter-policy.ts": "320e736a01bf82d95ab5bc0b8de97c635d71f7779925ff209e3064b01e145e72",
|
||||||
|
"https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/src/policies/hellthread-policy.ts": "965469606bdbb04b4bb0c61f90b7f6f0d073e394fa271e17784d2afde085476e",
|
||||||
|
"https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/src/policies/keyword-policy.ts": "c88db7137d336631b4fcc3532c5059c4a1e27caa50d6332a5fb593bf295d28df",
|
||||||
|
"https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/src/policies/noop-policy.ts": "e4164ab252c328d3ec72310d458cdcfc85bfbfdb7504f41e1d9ab4fd6fdcf4ef",
|
||||||
|
"https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/src/policies/openai-policy.ts": "cde09abe6dbdebdbb77ea13731a27ce8bcacbbd1fb21760d7784878dca587d81",
|
||||||
|
"https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/src/policies/pow-policy.ts": "d667623a4570e888d0cfdb41bf99bbbac0eb44eab5d97f5be1eeb190e06d34cb",
|
||||||
|
"https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/src/policies/pubkey-ban-policy.ts": "af2e3d6f5266bcb1785325a004a0a92088d18fa2433760f807158314184a82c9",
|
||||||
|
"https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/src/policies/rate-limit-policy.ts": "02e8539f30e67f7f7541628120358d70c4b05f362b4f21bbcceda475a6d3e357",
|
||||||
|
"https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/src/policies/read-only-policy.ts": "ec849ed7b06133bc11e3ce40412dd58469838376764a4326ffc043ea985c9739",
|
||||||
|
"https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/src/policies/regex-policy.ts": "626f7d4eb61eace9aa685a4f51b0b142b30abc96554ac5e375bbf3dc2a5ab685",
|
||||||
|
"https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/src/policies/whitelist-policy.ts": "f5cb4f616dc41c88505eb45adb2b2102a284ae7351ce9f76a76d53dd7b8bf575",
|
||||||
|
"https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/src/types.ts": "792aa1196dd290d815081ef874f8e66dacde344c9e30a8bf9031a1ebeb1da21d",
|
||||||
|
"https://raw.githubusercontent.com/alexgleason/Keydb/1bda308df9e589339532daf31f1717ef7a59d2af/adapter.ts": "32e5182648011b188952ada0528f564b374260449ec3b06237f36225d4d19510",
|
||||||
|
"https://raw.githubusercontent.com/alexgleason/Keydb/1bda308df9e589339532daf31f1717ef7a59d2af/jsonb.ts": "1b540f8bd0b43fe847cd3e2a852d2f53e610cd77b81c11d175ebe91a3f110be8",
|
||||||
|
"https://raw.githubusercontent.com/alexgleason/Keydb/1bda308df9e589339532daf31f1717ef7a59d2af/keydb.ts": "616c4c866c9e11c29d5654d367468ed51b689565043f53fdeb5eb66f25138156",
|
||||||
|
"https://raw.githubusercontent.com/alexgleason/Keydb/1bda308df9e589339532daf31f1717ef7a59d2af/memory.ts": "f0ab6faf293c4ad3539fd3cf89c764d7f34d39d24e471ea59eebb5d1f5a510dc",
|
||||||
|
"https://raw.githubusercontent.com/alexgleason/Keydb/1bda308df9e589339532daf31f1717ef7a59d2af/sqlite.ts": "c8f172cfea9425cb16e844622375c9578db508de7d710ad3987cf6cd6bff197a"
|
||||||
|
},
|
||||||
|
"workspace": {
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@nostr/tools@^2.3.1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
77
extras/strfry/ldap-policy.ts
Normal file
77
extras/strfry/ldap-policy.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import type { IterablePubkeys, Policy } from 'https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/mod.ts';
|
||||||
|
import { Client } from 'npm:ldapts';
|
||||||
|
import { nip57 } from '@nostr/tools';
|
||||||
|
|
||||||
|
interface LdapConfig {
|
||||||
|
url: string;
|
||||||
|
bindDN: string;
|
||||||
|
password: string;
|
||||||
|
searchDN: string;
|
||||||
|
whitelistPubkeys?: IterablePubkeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ldapPolicy: Policy<LdapConfig> = async (msg, opts) => {
|
||||||
|
const client = new Client({ url: opts.url });
|
||||||
|
const { kind, tags } = msg.event;
|
||||||
|
let { pubkey } = msg.event;
|
||||||
|
let out = { id: msg.event.id }
|
||||||
|
|
||||||
|
if (opts.whitelistPubkeys.includes(pubkey)) {
|
||||||
|
out['action'] = 'accept';
|
||||||
|
out['msg'] = '';
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zap receipt
|
||||||
|
if (kind === 9735) {
|
||||||
|
const descriptionTag = tags.find(([t, v]) => t === 'description' && v);
|
||||||
|
const invalidZapRequestMsg = 'Zap receipts must contain a valid zap request from a relay member';
|
||||||
|
|
||||||
|
if (typeof descriptionTag === 'undefined') {
|
||||||
|
out['action'] = 'reject';
|
||||||
|
out['msg'] = invalidZapRequestMsg;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
const zapRequestJSON = descriptionTag[1];
|
||||||
|
const validationResult = nip57.validateZapRequest(zapRequestJSON);
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// The zap receipt event's pubkey MUST be the same as the recipient's lnurl provider's nostrPubkey (retrieved in step 1 of the protocol flow).
|
||||||
|
// The invoiceAmount contained in the bolt11 tag of the zap receipt MUST equal the amount tag of the zap request (if present).
|
||||||
|
|
||||||
|
if (validationResult === null) {
|
||||||
|
pubkey = JSON.parse(zapRequestJSON).pubkey;
|
||||||
|
} else {
|
||||||
|
out['action'] = 'reject';
|
||||||
|
out['msg'] = invalidZapRequestMsg;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.bind(opts.bindDN, opts.password);
|
||||||
|
|
||||||
|
const { searchEntries } = await client.search(opts.searchDN, {
|
||||||
|
filter: `(nostrKey=${pubkey})`,
|
||||||
|
attributes: ['nostrKey']
|
||||||
|
});
|
||||||
|
const memberKey = searchEntries[0]?.nostrKey;
|
||||||
|
|
||||||
|
if (memberKey === pubkey) {
|
||||||
|
out['action'] = 'accept';
|
||||||
|
out['msg'] = '';
|
||||||
|
} else {
|
||||||
|
out['action'] = 'reject';
|
||||||
|
out['msg'] = 'Only members can publish notes on this relay';
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
out['action'] = 'reject';
|
||||||
|
out['msg'] = 'Auth service temporarily unavailable';
|
||||||
|
} finally {
|
||||||
|
await client.unbind();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ldapPolicy;
|
||||||
34
extras/strfry/strfry-policy.ts
Executable file
34
extras/strfry/strfry-policy.ts
Executable file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
//bin/true; exec deno run -A "$0" "$@"
|
||||||
|
import {
|
||||||
|
antiDuplicationPolicy,
|
||||||
|
hellthreadPolicy,
|
||||||
|
pipeline,
|
||||||
|
rateLimitPolicy,
|
||||||
|
readStdin,
|
||||||
|
writeStdout,
|
||||||
|
} from 'https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/mod.ts';
|
||||||
|
import ldapPolicy from './ldap-policy.ts';
|
||||||
|
import { load } from "https://deno.land/std@0.224.0/dotenv/mod.ts";
|
||||||
|
|
||||||
|
const dirname = new URL('.', import.meta.url).pathname;
|
||||||
|
await load({ envPath: `${dirname}/.env`, export: true });
|
||||||
|
|
||||||
|
const ldapConfig = {
|
||||||
|
url: Deno.env.get("LDAP_URL"),
|
||||||
|
bindDN: Deno.env.get("LDAP_BIND_DN"),
|
||||||
|
password: Deno.env.get("LDAP_PASSWORD"),
|
||||||
|
searchDN: Deno.env.get("LDAP_SEARCH_DN"),
|
||||||
|
whitelistPubkeys: Deno.env.get("WHITELIST_PUBKEYS")?.split(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const msg of readStdin()) {
|
||||||
|
const result = await pipeline(msg, [
|
||||||
|
[hellthreadPolicy, { limit: 10 }],
|
||||||
|
[antiDuplicationPolicy, { ttl: 60000, minLength: 50 }],
|
||||||
|
[rateLimitPolicy, { whitelist: ['127.0.0.1'] }],
|
||||||
|
[ldapPolicy, ldapConfig],
|
||||||
|
]);
|
||||||
|
|
||||||
|
writeStdout(result);
|
||||||
|
}
|
||||||
39
extras/strfry/strfry-sync.ts
Normal file
39
extras/strfry/strfry-sync.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { load } from "https://deno.land/std@0.224.0/dotenv/mod.ts";
|
||||||
|
import { Client } from 'npm:ldapts';
|
||||||
|
|
||||||
|
const dirname = new URL('.', import.meta.url).pathname;
|
||||||
|
await load({ envPath: `${dirname}/.env`, export: true });
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
url: Deno.env.get("LDAP_URL"),
|
||||||
|
bindDN: Deno.env.get("LDAP_BIND_DN"),
|
||||||
|
password: Deno.env.get("LDAP_PASSWORD"),
|
||||||
|
searchDN: Deno.env.get("LDAP_SEARCH_DN"),
|
||||||
|
relayUrl: Deno.args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new Client({ url: opts.url });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.bind(opts.bindDN, opts.password);
|
||||||
|
|
||||||
|
const { searchEntries } = await client.search(opts.searchDN, {
|
||||||
|
filter: `(nostrKey=*)`,
|
||||||
|
attributes: ['nostrKey']
|
||||||
|
});
|
||||||
|
|
||||||
|
const pubkeys = searchEntries.map(e => e.nostrKey);
|
||||||
|
const filter = JSON.stringify({ authors: pubkeys });
|
||||||
|
|
||||||
|
const p = Deno.run({ cmd: [
|
||||||
|
"strfry", "sync", opts.relayUrl,
|
||||||
|
"--dir", "down", "--filter", filter
|
||||||
|
]});
|
||||||
|
|
||||||
|
const result = await p.status();
|
||||||
|
|
||||||
|
Deno.exit(result.code);
|
||||||
|
} catch (ex) {
|
||||||
|
console.error(ex);
|
||||||
|
Deno.exit(1);
|
||||||
|
}
|
||||||
55
lib/nostr/event_kind.rb
Normal file
55
lib/nostr/event_kind.rb
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Nostr
|
||||||
|
# Defines the event kinds that can be emitted by clients.
|
||||||
|
module EventKind
|
||||||
|
# The content is set to a stringified JSON object +{name: <username>, about: <string>,
|
||||||
|
# picture: <url, string>}+ describing the user who created the event. A relay may delete past set_metadata
|
||||||
|
# events once it gets a new one for the same pubkey.
|
||||||
|
#
|
||||||
|
# @return [Integer]
|
||||||
|
#
|
||||||
|
SET_METADATA = 0
|
||||||
|
|
||||||
|
# The content is set to the text content of a note (anything the user wants to say).
|
||||||
|
# Non-plaintext notes should instead use kind 1000-10000 as described in NIP-16.
|
||||||
|
#
|
||||||
|
# @return [Integer]
|
||||||
|
#
|
||||||
|
TEXT_NOTE = 1
|
||||||
|
|
||||||
|
# The content is set to the URL (e.g., wss://somerelay.com) of a relay the event creator wants to
|
||||||
|
# recommend to its followers.
|
||||||
|
#
|
||||||
|
# @return [Integer]
|
||||||
|
#
|
||||||
|
RECOMMEND_SERVER = 2
|
||||||
|
|
||||||
|
# A special event with kind 3, meaning "contact list" is defined as having a list of p tags, one for each of
|
||||||
|
# the followed/known profiles one is following.
|
||||||
|
#
|
||||||
|
# @return [Integer]
|
||||||
|
#
|
||||||
|
CONTACT_LIST = 3
|
||||||
|
|
||||||
|
# A special event with kind 4, meaning "encrypted direct message". An event of this kind has its +content+
|
||||||
|
# equal to the base64-encoded, aes-256-cbc encrypted string of anything a user wants to write, encrypted using a
|
||||||
|
# shared cipher generated by combining the recipient's public-key with the sender's private-key.
|
||||||
|
#
|
||||||
|
# @return [Integer]
|
||||||
|
#
|
||||||
|
ENCRYPTED_DIRECT_MESSAGE = 4
|
||||||
|
|
||||||
|
# NIP-57 Zap request
|
||||||
|
#
|
||||||
|
# @return [Integer]
|
||||||
|
#
|
||||||
|
ZAP_REQUEST = 9734
|
||||||
|
|
||||||
|
# NIP-57 Zap receipt
|
||||||
|
#
|
||||||
|
# @return [Integer]
|
||||||
|
#
|
||||||
|
ZAP_RECEIPT = 9735
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
require "digest"
|
||||||
|
require "pp"
|
||||||
|
|
||||||
namespace :lndhub do
|
namespace :lndhub do
|
||||||
desc "Generate wallets for all users"
|
desc "Generate wallets for all users"
|
||||||
task :generate_wallets => :environment do |t, args|
|
task :generate_wallets => :environment do |t, args|
|
||||||
@@ -22,6 +25,21 @@ namespace :lndhub do
|
|||||||
puts "--\nSum of user balances: #{sum} sats"
|
puts "--\nSum of user balances: #{sum} sats"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc "Create test invoice"
|
||||||
|
task :create_test_invoice, [:username, :amount, :description] => :environment do |t, args|
|
||||||
|
user = User.find_by cn: args[:username]
|
||||||
|
desc = args[:description]
|
||||||
|
hash = Digest::SHA256.hexdigest(desc)
|
||||||
|
|
||||||
|
invoice = LndhubManager::CreateUserInvoice.call(user: user, payload: {
|
||||||
|
"amount": args[:amount].to_i,
|
||||||
|
"description": desc,
|
||||||
|
"description_hash": hash
|
||||||
|
})
|
||||||
|
|
||||||
|
pp invoice
|
||||||
|
end
|
||||||
|
|
||||||
desc "Migrate existing accounts to lndhub.go"
|
desc "Migrate existing accounts to lndhub.go"
|
||||||
task :migrate => :environment do |t, args|
|
task :migrate => :environment do |t, args|
|
||||||
# user = User.find_by cn: "jimmy"
|
# user = User.find_by cn: "jimmy"
|
||||||
|
|||||||
BIN
public/img/logos/icon_geary.png
Normal file
BIN
public/img/logos/icon_geary.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
11
public/img/logos/icon_mail.svg
Normal file
11
public/img/logos/icon_mail.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg version="1.1" viewBox="-5 -10 110 135" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m63.57 53.199-5.5742 5.5742c-2.1211 2.1211-4.9961 3.3125-7.9961 3.3125s-5.875-1.1914-8-3.3125l-5.5742-5.5742-30.066 30.066c0.90625 0.43359 1.9023 0.66406 2.9258 0.66406h81.43c1.0234 0 2.0195-0.23047 2.9258-0.66406z"
|
||||||
|
style="fill:#4f97ef;fill-opacity:1" />
|
||||||
|
<path d="m96.836 19.934c0.43359 0.90234 0.66406 1.9023 0.66406 2.9219v54.285c0 1.0234-0.23047 2.0195-0.66406 2.9258l-30.066-30.066z"
|
||||||
|
style="fill:#4f97ef;fill-opacity:1" />
|
||||||
|
<path d="m3.1641 19.934 30.066 30.066-30.066 30.066c-0.43359-0.90625-0.66406-1.9023-0.66406-2.9258v-54.285c0-1.0195 0.23047-2.0195 0.66406-2.9219z"
|
||||||
|
style="fill:#4f97ef;fill-opacity:1" />
|
||||||
|
<path d="m93.641 16.734c-0.90625-0.43359-1.9023-0.66406-2.9258-0.66406h-81.43c-1.0234 0-2.0195 0.23047-2.9258 0.66406l38.84 38.84c1.2734 1.2734 3 1.9883 4.8008 1.9883s3.5273-0.71484 4.7969-1.9883z"
|
||||||
|
style="fill:#4f97ef;fill-opacity:1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1018 B |
67
public/img/logos/icon_phanpy.svg
Normal file
67
public/img/logos/icon_phanpy.svg
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="54"
|
||||||
|
height="54"
|
||||||
|
viewBox="0 0 54 54"
|
||||||
|
version="1.1"
|
||||||
|
xml:space="preserve"
|
||||||
|
style="clip-rule:evenodd;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"
|
||||||
|
id="svg4"
|
||||||
|
sodipodi:docname="icon_phanpy.svg"
|
||||||
|
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:serif="http://www.serif.com/"><defs
|
||||||
|
id="defs4" /><sodipodi:namedview
|
||||||
|
id="namedview4"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="true"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="16.359375"
|
||||||
|
inkscape:cx="26.192932"
|
||||||
|
inkscape:cy="24.542502"
|
||||||
|
inkscape:window-width="2160"
|
||||||
|
inkscape:window-height="1281"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg4" />
|
||||||
|
<rect
|
||||||
|
id="Logo-simple"
|
||||||
|
serif:id="Logo simple"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="63.993999"
|
||||||
|
height="63.993999"
|
||||||
|
style="fill:none" />
|
||||||
|
<g
|
||||||
|
id="Logo-simple1"
|
||||||
|
serif:id="Logo simple"
|
||||||
|
transform="translate(-5.123639,-4.9968626)">
|
||||||
|
<g
|
||||||
|
id="g4">
|
||||||
|
<path
|
||||||
|
d="m 37.774,11.471 c 14.639,3.752 19.034,16.557 15.889,31.304 -0.696,3.261 -2.563,6.661 -6.356,8.693 -3.204,1.717 -8.07,2.537 -15.338,0.55 0,0 -9.634,-2.404 -9.634,-2.404 C 11.651,46.992 8.378,38.733 10.027,31.823 13.654,16.622 25.57,8.343 37.774,11.471 Z"
|
||||||
|
style="fill:#a4bff7"
|
||||||
|
id="path1" />
|
||||||
|
<path
|
||||||
|
d="m 36.76,15.429 c 12.289,3.15 15.547,14.114 12.907,26.493 -0.947,4.44 -4.937,9.365 -16.664,6.143 L 23.319,45.648 C 15.465,43.725 12.789,37.848 14.001,32.771 17.017,20.132 26.612,12.828 36.76,15.429 Z"
|
||||||
|
style="fill:#d8e7fe"
|
||||||
|
id="path2" />
|
||||||
|
<path
|
||||||
|
d="m 27.471,24.991 c -1.457,-0.698 -7.229,3.213 -7.663,8.926 -0.182,2.39 4.55,3.237 5.071,-0.169 0.725,-4.743 3.715,-8.218 2.592,-8.757 z"
|
||||||
|
style="fill:#6081e6"
|
||||||
|
id="path3" />
|
||||||
|
<path
|
||||||
|
d="m 38.217,26.996 c -2.083,0.327 -0.382,5.901 -0.595,10.727 -0.123,2.8 4.388,3.464 4.703,2.011 1.098,-5.073 -2.066,-13.058 -4.108,-12.738 z"
|
||||||
|
style="fill:#6081e6"
|
||||||
|
id="path4" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
18
public/img/logos/icon_remotestorage.svg
Normal file
18
public/img/logos/icon_remotestorage.svg
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<!-- Creator: CorelDRAW X7 -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="0.739008in" height="0.853339in" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
|
||||||
|
viewBox="0 0 739 853"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<defs>
|
||||||
|
<style type="text/css">
|
||||||
|
<![CDATA[
|
||||||
|
.fil0 {fill:#FF4B03}
|
||||||
|
]]>
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g id="Layer_x0020_1">
|
||||||
|
<metadata id="CorelCorpID_0Corel-Layer"/>
|
||||||
|
<polygon class="fil0" points="370,754 0,542 0,640 185,747 370,853 554,747 739,640 739,525 739,525 739,476 739,427 739,378 653,427 370,589 86,427 86,427 86,361 185,418 370,524 554,418 653,361 739,311 739,213 739,213 554,107 370,0 185,107 58,180 144,230 228,181 370,100 511,181 652,263 370,425 87,263 87,263 0,213 0,213 0,311 0,378 0,427 0,476 86,525 185,582 370,689 554,582 653,525 653,590 653,592 "/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
20
spec/factories/zaps.rb
Normal file
20
spec/factories/zaps.rb
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
FactoryBot.define do
|
||||||
|
factory :zap do
|
||||||
|
user { nil }
|
||||||
|
request {
|
||||||
|
Nostr::Event.new(
|
||||||
|
id: "3cf02d7f0ccd9711c25098fc50b3a7ab880326e4e51cc8c7a7b59f147cff4fff",
|
||||||
|
pubkey: "730b43e6f62c2ab22710b046e481802c8ac1108ed2cb9c21dff808d57ba24b6c",
|
||||||
|
created_at: 1712487443,
|
||||||
|
kind: 9734,
|
||||||
|
tags: [
|
||||||
|
["relays", "wss://nostr.kosmos.org", "wss://relay.example.com"],
|
||||||
|
["p", "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"]],
|
||||||
|
content: "",
|
||||||
|
sig: "e9e9bb2bac4267a107ab5c3368f504b4f11b8e3b5ae875a1d63c74d6934138d2521dc35815b6f534fc5d803cbf633736d871886368bb8f92c4ad3837a68a06f2"
|
||||||
|
).to_h
|
||||||
|
}
|
||||||
|
payment_request { "lnbc1u1p3mull3pp5qw4x46ew6kjknudypyjsg8maw935tr5kkuz7t6h7pugp3pt4msyqhp5zp40yd97a028sgr9x4yxq5d57gft6vwja58e8cl0eea4uasr6apscqzpgxqyz5vqsp53m2n8h6yeflgukv5fhwm802kur6un9w8nvycl7auk67w5g2u008q9qyyssqml8rfmxyvp32qd5939qx7uu0w6ppjuujlpwsrz28m9u0dzp799hz5j72w0xm8pg97hd4hdvwh9zxaw2hewnnmzewvc550f9y3qsfaegphmk0mu" }
|
||||||
|
receipt { nil }
|
||||||
|
end
|
||||||
|
end
|
||||||
19
spec/fixtures/lndhub/incoming-zap.json
vendored
Normal file
19
spec/fixtures/lndhub/incoming-zap.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"id": 58,
|
||||||
|
"type": "incoming",
|
||||||
|
"user_login": "123456abcdef",
|
||||||
|
"amount": 21000,
|
||||||
|
"fee": 0,
|
||||||
|
"memo": "Zap for satoshi@kosmos.org",
|
||||||
|
"description_hash": "b1a9910724bc9c1b03b4eba2e2d78ac69a8ac7b244e6ff6d4e7391bf6893f26a",
|
||||||
|
"payment_request": "lnbc1u1p3mull3pp5qw4x46ew6kjknudypyjsg8maw935tr5kkuz7t6h7pugp3pt4msyqhp5zp40yd97a028sgr9x4yxq5d57gft6vwja58e8cl0eea4uasr6apscqzpgxqyz5vqsp53m2n8h6yeflgukv5fhwm802kur6un9w8nvycl7auk67w5g2u008q9qyyssqml8rfmxyvp32qd5939qx7uu0w6ppjuujlpwsrz28m9u0dzp799hz5j72w0xm8pg97hd4hdvwh9zxaw2hewnnmzewvc550f9y3qsfaegphmk0mu",
|
||||||
|
"destination_pubkey_hex": "024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946",
|
||||||
|
"r_hash": "03aa6aeb2ed5a569f1a40925041f7d7163458e96b705e5eafe0f10188575dc08",
|
||||||
|
"preimage": "3539663535656537343331663432653165396430623966633664656664646563",
|
||||||
|
"keysend": false,
|
||||||
|
"state": "settled",
|
||||||
|
"created_at": "2023-01-11T09:22:57.546364Z",
|
||||||
|
"expires_at": "2023-01-12T09:22:57.547209Z",
|
||||||
|
"updated_at": "2023-01-11T09:22:58.046236131Z",
|
||||||
|
"settled_at": "2023-01-11T09:22:58.046232174Z"
|
||||||
|
}
|
||||||
9
spec/fixtures/nostr/valid_auth_event.json
vendored
Normal file
9
spec/fixtures/nostr/valid_auth_event.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"id": "7cc165c4fe4c9ec3f2b859cb422f01b38beaf6bbd228fea928ea1400ec254a89",
|
||||||
|
"pubkey": "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
|
||||||
|
"created_at": 1711963922,
|
||||||
|
"kind": 22242,
|
||||||
|
"tags": [["site","accounts.kosmos.org"],["challenge","YMeTyOxIEJcfe6vd"]],
|
||||||
|
"content": "",
|
||||||
|
"sig": "b484a28cd9c92facca0eba80e8ef5303d25ed044c3815e3a068b9887f91d3546ad209a0dd674c59b48cf8057aecd75df5416973d17ed58f68195030af09c28d1"
|
||||||
|
}
|
||||||
22
spec/fixtures/nostr/zap_request_event.json
vendored
Normal file
22
spec/fixtures/nostr/zap_request_event.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"kind": 9734,
|
||||||
|
"created_at": 1712066899,
|
||||||
|
"content": "",
|
||||||
|
"tags": [
|
||||||
|
["p", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"],
|
||||||
|
["amount", "21000"],
|
||||||
|
[
|
||||||
|
"relays",
|
||||||
|
"wss://relay.primal.net",
|
||||||
|
"wss://relay.snort.social",
|
||||||
|
"wss://nostr.wine",
|
||||||
|
"wss://relay.damus.io",
|
||||||
|
"wss://eden.nostr.land",
|
||||||
|
"wss://atlas.nostr.land",
|
||||||
|
"wss://nostr.bitcoiner.social",
|
||||||
|
"wss://relay.mostr.pub",
|
||||||
|
"wss://relay.mostr.pub/",
|
||||||
|
"wss://nostr-01.bolt.observer"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -3,12 +3,24 @@ require 'rails_helper'
|
|||||||
RSpec.describe CreateLdapUserJob, type: :job do
|
RSpec.describe CreateLdapUserJob, type: :job do
|
||||||
let(:ldap_client_mock) { instance_double(Net::LDAP) }
|
let(:ldap_client_mock) { instance_double(Net::LDAP) }
|
||||||
|
|
||||||
subject(:job) {
|
before do
|
||||||
allow_any_instance_of(described_class).to receive(:ldap_client).and_return(ldap_client_mock)
|
allow_any_instance_of(described_class).to receive(:ldap_client).and_return(ldap_client_mock)
|
||||||
|
end
|
||||||
|
|
||||||
|
subject(:job) {
|
||||||
described_class.perform_later(
|
described_class.perform_later(
|
||||||
'halfinney', 'kosmos.org', 'halfinney@example.com',
|
username: 'halfinney', domain: 'kosmos.org',
|
||||||
'remember-remember-the-5th-of-november'
|
email: 'halfinney@example.com',
|
||||||
|
hashed_pw: 'remember-remember-the-5th-of-november'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
subject(:job_for_preconfirmed_account) {
|
||||||
|
described_class.perform_later(
|
||||||
|
username: 'halfinney', domain: 'kosmos.org',
|
||||||
|
email: 'halfinney@example.com',
|
||||||
|
hashed_pw: 'remember-remember-the-5th-of-november',
|
||||||
|
confirmed: true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +42,26 @@ RSpec.describe CreateLdapUserJob, type: :job do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "adds default services for pre-confirmed accounts" do
|
||||||
|
allow(ldap_client_mock).to receive(:add) # spy on mock
|
||||||
|
allow(Setting).to receive(:default_services).and_return(["xmpp", "discourse"])
|
||||||
|
|
||||||
|
perform_enqueued_jobs { job_for_preconfirmed_account }
|
||||||
|
|
||||||
|
expect(ldap_client_mock).to have_received(:add).with(
|
||||||
|
dn: "cn=halfinney,ou=kosmos.org,cn=users,dc=kosmos,dc=org",
|
||||||
|
attributes: {
|
||||||
|
objectclass: ["top", "account", "person", "extensibleObject"],
|
||||||
|
cn: "halfinney",
|
||||||
|
sn: "halfinney",
|
||||||
|
uid: "halfinney",
|
||||||
|
mail: "halfinney@example.com",
|
||||||
|
serviceEnabled: ["xmpp", "discourse"],
|
||||||
|
userPassword: "remember-remember-the-5th-of-november"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
after do
|
after do
|
||||||
clear_enqueued_jobs
|
clear_enqueued_jobs
|
||||||
clear_performed_jobs
|
clear_performed_jobs
|
||||||
|
|||||||
@@ -38,4 +38,15 @@ RSpec.describe UserPreferences, type: :model do
|
|||||||
expect(res['lightning_notify_sats_received_threshold']).to eq(1000)
|
expect(res['lightning_notify_sats_received_threshold']).to eq(1000)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe ".pref_keys" do
|
||||||
|
let(:default_prefs) { YAML.load_file("#{Rails.root}/config/default_preferences.yml") }
|
||||||
|
|
||||||
|
it "returns the keys of all default preferences as an array of symbols" do
|
||||||
|
expect(UserPreferences.pref_keys).to be_a(Array)
|
||||||
|
expect(UserPreferences.pref_keys).to include(:lightning_notify_sats_received)
|
||||||
|
expect(UserPreferences.pref_keys).to include(:xmpp_exchange_contacts_with_invitees)
|
||||||
|
expect(UserPreferences.pref_keys.length).to eq(default_prefs.keys.length)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ RSpec.describe User, type: :model do
|
|||||||
let(:user) { build :user, cn: "jimmy", ou: "kosmos.org" }
|
let(:user) { build :user, cn: "jimmy", ou: "kosmos.org" }
|
||||||
|
|
||||||
context "Mastodon service not configured" do
|
context "Mastodon service not configured" do
|
||||||
|
before do
|
||||||
|
Setting.mastodon_enabled = false
|
||||||
|
end
|
||||||
|
|
||||||
it "returns nil" do
|
it "returns nil" do
|
||||||
expect(user.mastodon_address).to be_nil
|
expect(user.mastodon_address).to be_nil
|
||||||
end
|
end
|
||||||
@@ -41,6 +45,14 @@ RSpec.describe User, type: :model do
|
|||||||
expect(user.mastodon_address).to eq("jimmy@kosmos.social")
|
expect(user.mastodon_address).to eq("jimmy@kosmos.social")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "username contains hyphen/dash" do
|
||||||
|
let(:jammy) { build :user, cn: "jammy-jellyfish", ou: "kosmos.org" }
|
||||||
|
|
||||||
|
it "returns the user address" do
|
||||||
|
expect(jammy.mastodon_address).to eq("jammy_jellyfish@kosmos.org")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -72,6 +84,25 @@ RSpec.describe User, type: :model do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#service_enabled?" do
|
||||||
|
before do
|
||||||
|
allow(user).to receive(:ldap_entry).and_return({
|
||||||
|
uid: user.cn, ou: user.ou, mail: user.email, admin: nil,
|
||||||
|
services_enabled: ["gitea", "xmpp"]
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns true or false" do
|
||||||
|
expect(user.service_enabled?("gitea")).to be(true)
|
||||||
|
expect(user.service_enabled?("email")).to be(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns false when service is not enabled" do
|
||||||
|
expect(user.service_enabled?(:gitea)).to be(true)
|
||||||
|
expect(user.service_enabled?(:email)).to be(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "#enable_service" do
|
describe "#enable_service" do
|
||||||
before do
|
before do
|
||||||
allow(user).to receive(:ldap_entry).and_return({
|
allow(user).to receive(:ldap_entry).and_return({
|
||||||
@@ -147,7 +178,7 @@ RSpec.describe User, type: :model do
|
|||||||
after { clear_enqueued_jobs }
|
after { clear_enqueued_jobs }
|
||||||
|
|
||||||
it "enables default services" do
|
it "enables default services" do
|
||||||
expect(user).to receive(:enable_service).with(%w[ discourse gitea mediawiki xmpp ])
|
expect(user).to receive(:enable_service).with(%w[ discourse gitea mastodon mediawiki xmpp ])
|
||||||
user.send :devise_after_confirmation
|
user.send :devise_after_confirmation
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
12
spec/models/zap_spec.rb
Normal file
12
spec/models/zap_spec.rb
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Zap, type: :model do
|
||||||
|
describe "#request_event" do
|
||||||
|
let(:user) { create :user, cn: 'satoshi', ou: 'kosmos.org', ln_account: 'abcdefg123456' }
|
||||||
|
let(:zap) { create :zap, user: user }
|
||||||
|
|
||||||
|
it "returns the stored request as a Nostr::Event" do
|
||||||
|
expect(zap.request_event).to be_a(Nostr::Event)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe "/lnurlpay", type: :request do
|
RSpec.describe "/lnurlpay", type: :request do
|
||||||
|
|
||||||
context "Non-existent user" do
|
context "Non-existent user" do
|
||||||
describe "GET /.well-known/lnurlpay/:username" do
|
describe "GET /.well-known/lnurlp/:username" do
|
||||||
it "returns a 404" do
|
it "returns a 404" do
|
||||||
get lightning_address_path(username: "csw")
|
get lightning_address_path(username: "csw")
|
||||||
expect(response).to have_http_status(:not_found)
|
expect(response).to have_http_status(:not_found)
|
||||||
@@ -30,9 +29,10 @@ RSpec.describe "/lnurlpay", type: :request do
|
|||||||
|
|
||||||
before do
|
before do
|
||||||
login_as user, :scope => :user
|
login_as user, :scope => :user
|
||||||
|
Setting.nostr_enabled = false
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "GET /.well-known/lnurlpay/:username" do
|
describe "GET /.well-known/lnurlp/:username" do
|
||||||
it "returns a formatted Lightning Address response" do
|
it "returns a formatted Lightning Address response" do
|
||||||
get lightning_address_path(username: "satoshi")
|
get lightning_address_path(username: "satoshi")
|
||||||
|
|
||||||
@@ -45,17 +45,41 @@ RSpec.describe "/lnurlpay", type: :request do
|
|||||||
expect(res["minSendable"]).to be_a(Integer)
|
expect(res["minSendable"]).to be_a(Integer)
|
||||||
expect(res["maxSendable"]).to be_a(Integer)
|
expect(res["maxSendable"]).to be_a(Integer)
|
||||||
expect(res["commentAllowed"]).to be_a(Integer)
|
expect(res["commentAllowed"]).to be_a(Integer)
|
||||||
|
expect(res["allowsNostr"]).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with support for nostr zaps" do
|
||||||
|
before do
|
||||||
|
Setting.nostr_enabled = true
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns NIP-57 properties" do
|
||||||
|
get lightning_address_path(username: "satoshi")
|
||||||
|
|
||||||
|
res = JSON.parse(response.body)
|
||||||
|
expect(res["allowsNostr"]).to be(true)
|
||||||
|
expect(res["nostrPubkey"]).to eq(Setting.nostr_public_key)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "GET /lnurlpay/:username/invoice" do
|
describe "GET /lnurlpay/:username/invoice" do
|
||||||
|
let(:invoice) {
|
||||||
|
{
|
||||||
|
"payment_hash" => "35778ccdb8319b5104e41d2043d18446bf91bcd51450b5f1cf1b6082a7cc6203",
|
||||||
|
"payment_request" => "lnbc210n1pnzzr6rpp5x4mcendcxxd4zp8yr5sy85vyg6ler0x4z3gttuw0rdsg9f7vvgpshp52y6nf64apaqta2kjuwp2apglewqa9fva2mada6x2mmdj20t57jdscqzzsxqyz5vqsp5a3h88efdc436wunupz293gdtvm5843yfcfc8hxm2rpdunaetl39q9qyyssq07ec02dqr4epa73ssy0lzwglw49aa9rfywlp0c7jpnf448uapsgqch79d4222xqlh8674lzddvcyptpnwqqq8vpppf8djrn8yjf53dqpzwx5kh",
|
||||||
|
"expires_at" => "2024-04-19T12:17:07.725314947Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow_any_instance_of(User).to receive(:ln_create_invoice).and_return("lnbc50u1p3lwgknpp52g78gqya5euvzjc53fc6hkmlm2rfjhcd305tcmc0g9gaestav48sdq4gdhkven9v5sx6mmwv4ujzcqzpgxqyz5vqsp5skkz4jlqr6tkvv2g9739ygrjupc4rkqd94mc7dfpj3pgx3f6w7qs9qyyssq7mf3fzcuxlmkr9nqatcch3u8uf4gjyawe052tejz8e9fqxu4pncqk3qklt8g6ylpshg09xyjquyrgtc72vcw5cp0dzcf406apyua7dgpnfn7an")
|
allow(LndhubManager::CreateUserInvoice).to receive(:call)
|
||||||
|
.and_return(invoice)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns a formatted lnurlpay response" do
|
it "returns a formatted lnurlpay response" do
|
||||||
get lnurlpay_invoice_path(username: "satoshi", params: {
|
get lnurlpay_invoice_path(username: "satoshi", params: {
|
||||||
amount: 50000, comment: "Coffee time!"
|
amount: 21000, comment: "Coffee time!"
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
@@ -64,32 +88,108 @@ RSpec.describe "/lnurlpay", type: :request do
|
|||||||
expect(res["status"]).to eq('OK')
|
expect(res["status"]).to eq('OK')
|
||||||
expect(res["successAction"]["tag"]).to eq('message')
|
expect(res["successAction"]["tag"]).to eq('message')
|
||||||
expect(res["successAction"]["message"]).to match('Thank you')
|
expect(res["successAction"]["message"]).to match('Thank you')
|
||||||
expect(res["pr"]).to eq("lnbc50u1p3lwgknpp52g78gqya5euvzjc53fc6hkmlm2rfjhcd305tcmc0g9gaestav48sdq4gdhkven9v5sx6mmwv4ujzcqzpgxqyz5vqsp5skkz4jlqr6tkvv2g9739ygrjupc4rkqd94mc7dfpj3pgx3f6w7qs9qyyssq7mf3fzcuxlmkr9nqatcch3u8uf4gjyawe052tejz8e9fqxu4pncqk3qklt8g6ylpshg09xyjquyrgtc72vcw5cp0dzcf406apyua7dgpnfn7an")
|
expect(res["pr"]).to eq("lnbc210n1pnzzr6rpp5x4mcendcxxd4zp8yr5sy85vyg6ler0x4z3gttuw0rdsg9f7vvgpshp52y6nf64apaqta2kjuwp2apglewqa9fva2mada6x2mmdj20t57jdscqzzsxqyz5vqsp5a3h88efdc436wunupz293gdtvm5843yfcfc8hxm2rpdunaetl39q9qyyssq07ec02dqr4epa73ssy0lzwglw49aa9rfywlp0c7jpnf448uapsgqch79d4222xqlh8674lzddvcyptpnwqqq8vpppf8djrn8yjf53dqpzwx5kh")
|
||||||
end
|
end
|
||||||
|
|
||||||
context "amount too low" do
|
describe "amount too low" do
|
||||||
it "returns an error" do
|
it "returns an error" do
|
||||||
get lnurlpay_invoice_path(username: "satoshi", params: {
|
get lnurlpay_invoice_path(username: "satoshi", params: {
|
||||||
amount: 5000, comment: "Coffee time!"
|
amount: 5000, comment: "Coffee time!"
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
res = JSON.parse(response.body)
|
res = JSON.parse(response.body)
|
||||||
expect(res["status"]).to eq('ERROR')
|
expect(res["status"]).to eq('ERROR')
|
||||||
expect(res["reason"]).to eq('Invalid amount')
|
expect(res["reason"]).to eq('Invalid amount')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "comment too long" do
|
describe "comment too long" do
|
||||||
it "returns an error" do
|
it "returns an error" do
|
||||||
get lnurlpay_invoice_path(username: "satoshi", params: {
|
get lnurlpay_invoice_path(username: "satoshi", params: {
|
||||||
amount: 5000000, comment: "Coffee time is the best time, so here's some money for you to get some. May I suggest to sample some Pacamara beans from El Salvador?"
|
amount: 5000000, comment: "Coffee time is the best time, so here's some money for you to get some. May I suggest to sample some Pacamara beans from El Salvador?"
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
res = JSON.parse(response.body)
|
res = JSON.parse(response.body)
|
||||||
expect(res["status"]).to eq('ERROR')
|
expect(res["status"]).to eq('ERROR')
|
||||||
expect(res["reason"]).to eq('Comment too long')
|
expect(res["reason"]).to eq('Comment too long')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "zap request" do
|
||||||
|
before do
|
||||||
|
Setting.nostr_enabled = true
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "with invalid request event" do
|
||||||
|
it "returns an error" do
|
||||||
|
get lnurlpay_invoice_path(username: "satoshi", params: {
|
||||||
|
amount: 2100000, nostr: { foo: "bar" }.to_json
|
||||||
|
})
|
||||||
|
|
||||||
|
res = JSON.parse(response.body)
|
||||||
|
expect(res["status"]).to eq('ERROR')
|
||||||
|
expect(res["reason"]).to eq('Invalid zap request')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "with valid request event" do
|
||||||
|
let(:event) {
|
||||||
|
Nostr::Event.new(
|
||||||
|
id: "3cf02d7f0ccd9711c25098fc50b3a7ab880326e4e51cc8c7a7b59f147cff4fff",
|
||||||
|
pubkey: "730b43e6f62c2ab22710b046e481802c8ac1108ed2cb9c21dff808d57ba24b6c",
|
||||||
|
created_at: 1712487443,
|
||||||
|
kind: 9734,
|
||||||
|
tags: [
|
||||||
|
["relays", "wss://nostr.kosmos.org", "wss://relay.example.com"],
|
||||||
|
["p", "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"]],
|
||||||
|
content: "",
|
||||||
|
sig: "e9e9bb2bac4267a107ab5c3368f504b4f11b8e3b5ae875a1d63c74d6934138d2521dc35815b6f534fc5d803cbf633736d871886368bb8f92c4ad3837a68a06f2"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let(:invoice) {
|
||||||
|
{
|
||||||
|
"payment_hash" => "d6fa1d4b66587ab247d7b1f7608f2aed780a0cca021d09a30279c3dd9ff80f85",
|
||||||
|
"payment_request" => "lnbc210n1pnzzyvjpp56map6jmxtpaty37hk8mkpre2a4uq5rx2qgwsngcz08pam8lcp7zshp5xs7v3qlx0j0gyu9grrzx9xgews3t9vq64v30579le9z9wqr6fc5scqzzsxqyz5vqsp5kmltj5eayh47c6trwj8wdrz5nxymqp0eqwtk7k5nk6ytyz522nvs9qyyssqvkluufkp34gtzxdg0uyqcsdum2n34xz94tqr4jfwwx53czteutvj7eptz4lm5vcu0m8jqzxck484ycxzcqgqlqmpj2r3jxjlj4x6nygp8fvnag",
|
||||||
|
"expires_at" => "2024-04-19T12:26:58.432434748Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it "returns an invoice" do
|
||||||
|
expect(LndhubManager::CreateUserInvoice).to receive(:call)
|
||||||
|
.with(user: user, payload: {
|
||||||
|
amount: 21,
|
||||||
|
description: "Zap for satoshi@kosmos.org",
|
||||||
|
description_hash: "b1a9910724bc9c1b03b4eba2e2d78ac69a8ac7b244e6ff6d4e7391bf6893f26a"
|
||||||
|
})
|
||||||
|
.and_return(invoice)
|
||||||
|
|
||||||
|
get lnurlpay_invoice_path(username: "satoshi", params: {
|
||||||
|
amount: 21000, nostr: event.to_json
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
res = JSON.parse(response.body)
|
||||||
|
expect(res["status"]).to eq('OK')
|
||||||
|
expect(res["pr"]).to eq(invoice["payment_request"])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "creates a zap record" do
|
||||||
|
get lnurlpay_invoice_path(username: "satoshi", params: {
|
||||||
|
amount: 21000, nostr: event.to_json
|
||||||
|
})
|
||||||
|
|
||||||
|
zap = user.zaps.find_by payment_request: invoice["payment_request"]
|
||||||
|
expect(zap.request_event.id).to eq(event.id)
|
||||||
|
expect(zap.receipt).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "GET /.well-known/keysend/:username/" do
|
describe "GET /.well-known/keysend/:username/" do
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ require 'rails_helper'
|
|||||||
RSpec.describe "Settings", type: :request do
|
RSpec.describe "Settings", type: :request do
|
||||||
let(:user) { create :user, cn: 'mark', ou: 'kosmos.org' }
|
let(:user) { create :user, cn: 'mark', ou: 'kosmos.org' }
|
||||||
let(:other_user) { create :user, id: 2, cn: 'markymark', ou: 'kosmos.org', email: 'markymark@interscope.com' }
|
let(:other_user) { create :user, id: 2, cn: 'markymark', ou: 'kosmos.org', email: 'markymark@interscope.com' }
|
||||||
|
let(:auth_event) { JSON.parse(File.read("#{Rails.root}/spec/fixtures/nostr/valid_auth_event.json")) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
login_as user, :scope => :user
|
login_as user, :scope => :user
|
||||||
@@ -25,7 +26,7 @@ RSpec.describe "Settings", type: :request do
|
|||||||
|
|
||||||
describe "POST /settings/set_nostr_pubkey" do
|
describe "POST /settings/set_nostr_pubkey" do
|
||||||
before do
|
before do
|
||||||
session_stub = { shared_secret: "rMjWEmvcvtTlQkMd" }
|
session_stub = { shared_secret: "YMeTyOxIEJcfe6vd" }
|
||||||
allow_any_instance_of(SettingsController).to receive(:session).and_return(session_stub)
|
allow_any_instance_of(SettingsController).to receive(:session).and_return(session_stub)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -36,19 +37,12 @@ RSpec.describe "Settings", type: :request do
|
|||||||
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"
|
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"
|
||||||
).and_return(0)
|
).and_return(0)
|
||||||
|
|
||||||
post set_nostr_pubkey_settings_path, params: {
|
post set_nostr_pubkey_settings_path,
|
||||||
signed_event: {
|
params: { signed_event: auth_event }.to_json,
|
||||||
id: "84f266bbd784551aaa9e35cb0aceb4ee59182a1dab9ab279d9e40dd56ecbbdd3",
|
headers: {
|
||||||
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
|
"CONTENT_TYPE" => "application/json",
|
||||||
created_at: 1678254161,
|
"HTTP_ACCEPT" => "application/json"
|
||||||
kind: 1,
|
|
||||||
content: "Connect my public key to mark@kosmos.org (confirmation rMjWEmvcvtTlQkMd)",
|
|
||||||
sig: "96796d420547d6e2c7be5de82a2ce7a48be99aac6415464a6081859ac1a9017305accc0228c630466a57d45ec1c3b456376eb538b76dfdaa2397e3258be02fdd"
|
|
||||||
}
|
}
|
||||||
}.to_json, headers: {
|
|
||||||
"CONTENT_TYPE" => "application/json",
|
|
||||||
"HTTP_ACCEPT" => "application/json"
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns a success status" do
|
it "returns a success status" do
|
||||||
@@ -67,19 +61,12 @@ RSpec.describe "Settings", type: :request do
|
|||||||
).and_return(other_user)
|
).and_return(other_user)
|
||||||
expect(LdapManager::UpdateNostrKey).not_to receive(:call)
|
expect(LdapManager::UpdateNostrKey).not_to receive(:call)
|
||||||
|
|
||||||
post set_nostr_pubkey_settings_path, params: {
|
post set_nostr_pubkey_settings_path,
|
||||||
signed_event: {
|
params: { signed_event: auth_event }.to_json,
|
||||||
id: "84f266bbd784551aaa9e35cb0aceb4ee59182a1dab9ab279d9e40dd56ecbbdd3",
|
headers: {
|
||||||
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
|
"CONTENT_TYPE" => "application/json",
|
||||||
created_at: 1678254161,
|
"HTTP_ACCEPT" => "application/json"
|
||||||
kind: 1,
|
|
||||||
content: "Connect my public key to mark@kosmos.org (confirmation rMjWEmvcvtTlQkMd)",
|
|
||||||
sig: "96796d420547d6e2c7be5de82a2ce7a48be99aac6415464a6081859ac1a9017305accc0228c630466a57d45ec1c3b456376eb538b76dfdaa2397e3258be02fdd"
|
|
||||||
}
|
}
|
||||||
}.to_json, headers: {
|
|
||||||
"CONTENT_TYPE" => "application/json",
|
|
||||||
"HTTP_ACCEPT" => "application/json"
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns a 422 status" do
|
it "returns a 422 status" do
|
||||||
@@ -91,23 +78,21 @@ RSpec.describe "Settings", type: :request do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "With wrong username" do
|
context "With wrong site tag" do
|
||||||
before do
|
before do
|
||||||
|
Setting.accounts_domain = "accounts.wikipedia.org"
|
||||||
expect(LdapManager::UpdateNostrKey).not_to receive(:call)
|
expect(LdapManager::UpdateNostrKey).not_to receive(:call)
|
||||||
|
|
||||||
post set_nostr_pubkey_settings_path, params: {
|
post set_nostr_pubkey_settings_path,
|
||||||
signed_event: {
|
params: { signed_event: auth_event }.to_json,
|
||||||
id: "2e1e20ee762d6a5b5b30835eda9ca03146e4baf82490e53fd75794c08de08ac0",
|
headers: {
|
||||||
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
|
"CONTENT_TYPE" => "application/json",
|
||||||
created_at: 1678255391,
|
"HTTP_ACCEPT" => "application/json"
|
||||||
kind: 1,
|
|
||||||
content: "Connect my public key to admin@kosmos.org (confirmation rMjWEmvcvtTlQkMd)",
|
|
||||||
sig: "2ace19c9db892ac6383848721a3e08b13d90d689fdeac60d9633a623d3f08eb7e0d468f1b3e928d1ea979477c2ec46ee6cdb2d053ef2e4ed3c0630a51d249029"
|
|
||||||
}
|
}
|
||||||
}.to_json, headers: {
|
end
|
||||||
"CONTENT_TYPE" => "application/json",
|
|
||||||
"HTTP_ACCEPT" => "application/json"
|
after do
|
||||||
}
|
Setting.accounts_domain = "accounts.kosmos.org"
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns a 422 status" do
|
it "returns a 422 status" do
|
||||||
@@ -126,19 +111,12 @@ RSpec.describe "Settings", type: :request do
|
|||||||
|
|
||||||
expect(LdapManager::UpdateNostrKey).not_to receive(:call)
|
expect(LdapManager::UpdateNostrKey).not_to receive(:call)
|
||||||
|
|
||||||
post set_nostr_pubkey_settings_path, params: {
|
post set_nostr_pubkey_settings_path,
|
||||||
signed_event: {
|
params: { signed_event: auth_event }.to_json,
|
||||||
id: "84f266bbd784551aaa9e35cb0aceb4ee59182a1dab9ab279d9e40dd56ecbbdd3",
|
headers: {
|
||||||
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
|
"CONTENT_TYPE" => "application/json",
|
||||||
created_at: 1678254161,
|
"HTTP_ACCEPT" => "application/json"
|
||||||
kind: 1,
|
|
||||||
content: "Connect my public key to mark@kosmos.org (confirmation rMjWEmvcvtTlQkMd)",
|
|
||||||
sig: "96796d420547d6e2c7be5de82a2ce7a48be99aac6415464a6081859ac1a9017305accc0228c630466a57d45ec1c3b456376eb538b76dfdaa2397e3258be02fdd"
|
|
||||||
}
|
}
|
||||||
}.to_json, headers: {
|
|
||||||
"CONTENT_TYPE" => "application/json",
|
|
||||||
"HTTP_ACCEPT" => "application/json"
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns a 422 status" do
|
it "returns a 422 status" do
|
||||||
|
|||||||
94
spec/requests/users/sessions_spec.rb
Normal file
94
spec/requests/users/sessions_spec.rb
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe "Devise login sessions", type: :request do
|
||||||
|
let(:user) { create :user, cn: 'fiatjaf', ou: 'kosmos.org' }
|
||||||
|
let(:auth_event) { JSON.parse(File.read("#{Rails.root}/spec/fixtures/nostr/valid_auth_event.json")) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
login_as user, :scope => :user
|
||||||
|
|
||||||
|
allow_any_instance_of(User).to receive(:dn)
|
||||||
|
.and_return("cn=#{user.cn},ou=kosmos.org,cn=users,dc=kosmos,dc=org")
|
||||||
|
allow_any_instance_of(User).to receive(:nostr_pubkey).and_return(nil)
|
||||||
|
|
||||||
|
allow(LdapManager::FetchUserByNostrKey).to receive(:call).with(
|
||||||
|
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"
|
||||||
|
).and_return(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "POST /users/nostr_login" do
|
||||||
|
before do
|
||||||
|
session_stub = { shared_secret: "YMeTyOxIEJcfe6vd" }
|
||||||
|
allow_any_instance_of(Users::SessionsController).to receive(:session).and_return(session_stub)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "With key configured for an account" do
|
||||||
|
before do
|
||||||
|
expect(LdapManager::FetchUserByNostrKey).to receive(:call).with(
|
||||||
|
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"
|
||||||
|
).and_return(user)
|
||||||
|
|
||||||
|
post users_nostr_login_path,
|
||||||
|
params: { signed_event: auth_event }.to_json,
|
||||||
|
headers: {
|
||||||
|
"CONTENT_TYPE" => "application/json",
|
||||||
|
"HTTP_ACCEPT" => "application/json"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns a success status" do
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "With wrong site tag" do
|
||||||
|
before do
|
||||||
|
Setting.accounts_domain = "accounts.wikipedia.org"
|
||||||
|
expect(LdapManager::FetchUserByNostrKey).not_to receive(:call)
|
||||||
|
|
||||||
|
post users_nostr_login_path,
|
||||||
|
params: { signed_event: auth_event }.to_json,
|
||||||
|
headers: {
|
||||||
|
"CONTENT_TYPE" => "application/json",
|
||||||
|
"HTTP_ACCEPT" => "application/json"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
Setting.accounts_domain = "accounts.kosmos.org"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns a 422 status" do
|
||||||
|
expect(response).to have_http_status(401)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "informs the user about the failure" do
|
||||||
|
expect(flash[:alert]).to eq("Login verification failed")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "With wrong shared secret" do
|
||||||
|
before do
|
||||||
|
session_stub = { shared_secret: "ho-chi-minh" }
|
||||||
|
allow_any_instance_of(Users::SessionsController).to receive(:session).and_return(session_stub)
|
||||||
|
|
||||||
|
expect(LdapManager::FetchUserByNostrKey).not_to receive(:call)
|
||||||
|
|
||||||
|
post users_nostr_login_path,
|
||||||
|
params: { signed_event: auth_event }.to_json,
|
||||||
|
headers: {
|
||||||
|
"CONTENT_TYPE" => "application/json",
|
||||||
|
"HTTP_ACCEPT" => "application/json"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns a 422 status" do
|
||||||
|
expect(response).to have_http_status(401)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "informs the user about the failure" do
|
||||||
|
expect(flash[:alert]).to eq("Login verification failed")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,13 +1,87 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe "WebFinger", type: :request do
|
RSpec.describe "WebFinger", type: :request do
|
||||||
describe "remoteStorage link relation" do
|
describe "User does not exist" do
|
||||||
context "user exists" do
|
it "returns a 404 status" do
|
||||||
before do
|
get "/.well-known/webfinger?resource=acct%3Ajane.doe%40kosmos.org"
|
||||||
create :user, cn: 'tony', ou: 'kosmos.org'
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "User exists" do
|
||||||
|
let(:user) { create :user, cn: 'tony', ou: 'kosmos.org' }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow_any_instance_of(User).to receive(:ldap_entry).and_return({
|
||||||
|
uid: user.cn, ou: user.ou, mail: user.email, admin: nil,
|
||||||
|
services_enabled: ["mastodon", "remotestorage"]
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Mastodon entries" do
|
||||||
|
context "Mastodon available" do
|
||||||
|
it "includes the Mastodon aliases and links for the user" do
|
||||||
|
get "/.well-known/webfinger?resource=acct%3Atony%40kosmos.org"
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
res = JSON.parse(response.body)
|
||||||
|
|
||||||
|
expect(res["aliases"]).to include("http://example.social/@tony")
|
||||||
|
expect(res["aliases"]).to include("http://example.social/users/tony")
|
||||||
|
|
||||||
|
profile_link = res["links"].find{|l| l["rel"] == "http://webfinger.net/rel/profile-page"}
|
||||||
|
self_link = res["links"].find{|l| l["rel"] == "self"}
|
||||||
|
ostatus_link = res["links"].find{|l| l["rel"] == "http://ostatus.org/schema/1.0/subscribe"}
|
||||||
|
expect(profile_link["type"]).to eql("text/html")
|
||||||
|
expect(profile_link["href"]).to eql("http://example.social/@tony")
|
||||||
|
expect(self_link["type"]).to eql("application/activity+json")
|
||||||
|
expect(self_link["href"]).to eql("http://example.social/users/tony")
|
||||||
|
expect(ostatus_link["template"]).to eql("http://example.social/authorize_interaction?uri={uri}")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "remoteStorage enabled globally" do
|
context "Mastodon not enabled for user" do
|
||||||
|
before do
|
||||||
|
allow_any_instance_of(User).to receive(:ldap_entry).and_return({
|
||||||
|
uid: user.cn, ou: user.ou, mail: user.email, admin: nil,
|
||||||
|
services_enabled: ["xmpp"]
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not include Mastodon aliases or links" do
|
||||||
|
get "/.well-known/webfinger?resource=acct%3Atony%40kosmos.org"
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
res = JSON.parse(response.body)
|
||||||
|
expect(res["aliases"]).not_to include("http://example.social/@tony")
|
||||||
|
expect(res["aliases"]).not_to include("http://example.social/users/tony")
|
||||||
|
expect(res["links"].find{|l| l["rel"] == "http://webfinger.net/rel/profile-page"}).to be(nil)
|
||||||
|
expect(res["links"].find{|l| l["rel"] == "self"}).to be(nil)
|
||||||
|
expect(res["links"].find{|l| l["rel"] == "http://ostatus.org/schema/1.0/subscribe"}).to be(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "Mastodon not available" do
|
||||||
|
before do
|
||||||
|
Setting.mastodon_enabled = false
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not include Mastodon aliases or links" do
|
||||||
|
get "/.well-known/webfinger?resource=acct%3Atony%40kosmos.org"
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
res = JSON.parse(response.body)
|
||||||
|
expect(res["aliases"]).not_to include("http://example.social/@tony")
|
||||||
|
expect(res["aliases"]).not_to include("http://example.social/users/tony")
|
||||||
|
expect(res["links"].find{|l| l["rel"] == "http://webfinger.net/rel/profile-page"}).to be(nil)
|
||||||
|
expect(res["links"].find{|l| l["rel"] == "self"}).to be(nil)
|
||||||
|
expect(res["links"].find{|l| l["rel"] == "http://ostatus.org/schema/1.0/subscribe"}).to be(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "remoteStorage entries" do
|
||||||
|
context "remoteStorage available" do
|
||||||
it "includes the remoteStorage link for the user" do
|
it "includes the remoteStorage link for the user" do
|
||||||
get "/.well-known/webfinger?resource=acct%3Atony%40kosmos.org"
|
get "/.well-known/webfinger?resource=acct%3Atony%40kosmos.org"
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
@@ -22,6 +96,25 @@ RSpec.describe "WebFinger", type: :request do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "remoteStorage not enabled for user" do
|
||||||
|
before do
|
||||||
|
allow_any_instance_of(User).to receive(:ldap_entry).and_return({
|
||||||
|
uid: user.cn, ou: user.ou, mail: user.email, admin: nil,
|
||||||
|
services_enabled: ["xmpp"]
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not include the remoteStorage link" do
|
||||||
|
get "/.well-known/webfinger?resource=acct%3Atony%40kosmos.org"
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
res = JSON.parse(response.body)
|
||||||
|
rs_link = res["links"].find {|l| l["rel"] == "http://tools.ietf.org/id/draft-dejong-remotestorage"}
|
||||||
|
|
||||||
|
expect(rs_link).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "remoteStorage not available" do
|
context "remoteStorage not available" do
|
||||||
before do
|
before do
|
||||||
Setting.remotestorage_enabled = false
|
Setting.remotestorage_enabled = false
|
||||||
@@ -38,12 +131,5 @@ RSpec.describe "WebFinger", type: :request do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "user does not exist" do
|
|
||||||
it "does return a 404 status" do
|
|
||||||
get "/.well-known/webfinger?resource=acct%3Ajane.doe%40kosmos.org"
|
|
||||||
expect(response).to have_http_status(:not_found)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -53,17 +53,16 @@ RSpec.describe "Webhooks", type: :request do
|
|||||||
let(:user) { create :user, ln_account: "123456abcdef" }
|
let(:user) { create :user, ln_account: "123456abcdef" }
|
||||||
let(:payload) { JSON.parse(File.read(File.expand_path("../fixtures/lndhub/incoming.json", File.dirname(__FILE__)))) }
|
let(:payload) { JSON.parse(File.read(File.expand_path("../fixtures/lndhub/incoming.json", File.dirname(__FILE__)))) }
|
||||||
|
|
||||||
before do
|
before { user.save! } #FIXME this should not be necessary
|
||||||
user.save! #FIXME this should not be necessary
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns a 200 status" do
|
it "returns a 200 status" do
|
||||||
post "/webhooks/lndhub", params: payload.to_json
|
post "/webhooks/lndhub", params: payload.to_json
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "does not send notifications by default" do
|
it "does not send a zap receipt" do
|
||||||
expect(enqueued_jobs.size).to eq(0)
|
expect(NostrManager::PublishZapReceipt).not_to receive(:call)
|
||||||
|
post "/webhooks/lndhub", params: payload.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
context "notification preference set to 'xmpp'" do
|
context "notification preference set to 'xmpp'" do
|
||||||
@@ -102,6 +101,97 @@ RSpec.describe "Webhooks", type: :request do
|
|||||||
expect(args[3]["params"]["amount_sats"]).to eq(12300)
|
expect(args[3]["params"]["amount_sats"]).to eq(12300)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "minimum threshold amount not reached" do
|
||||||
|
before do
|
||||||
|
user.update! preferences: {
|
||||||
|
lightning_notify_sats_received: "xmpp",
|
||||||
|
lightning_notify_min_sats: 21000
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not send a notification" do
|
||||||
|
post "/webhooks/lndhub", params: payload.to_json
|
||||||
|
expect(enqueued_jobs.size).to eq(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "no memo/description/message" do
|
||||||
|
before do
|
||||||
|
user.update! preferences: {
|
||||||
|
lightning_notify_sats_received: "xmpp",
|
||||||
|
lightning_notify_only_with_message: true
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not send a notification" do
|
||||||
|
post "/webhooks/lndhub", params: payload.merge({ memo: "" }).to_json
|
||||||
|
expect(enqueued_jobs.size).to eq(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Valid payload for zap transaction" do
|
||||||
|
let(:user) { create :user, ln_account: "123456abcdef" }
|
||||||
|
let(:zap) { create :zap, user: user }
|
||||||
|
let(:payload) { JSON.parse(File.read(File.expand_path("../fixtures/lndhub/incoming-zap.json", File.dirname(__FILE__)))) }
|
||||||
|
let(:zap_receipt) {
|
||||||
|
Nostr::Event.new(
|
||||||
|
id: "cb66278a9add37a2f1e018826327ff15304e8055ff7b910100225baf83a9d691",
|
||||||
|
sig: "a808d6792e21824bfddc98742b6831b1070e8b21e12aa424d2bb168a09f3a95a217d4513e803f2acb6e38404f763eb09fa07a341ee9c8c4c7d18bbe3d381eb6f",
|
||||||
|
pubkey: "bdd76ce2934b2f591f9fad2ebe9da18f20d2921de527494ba00eeaa0a0efadcf",
|
||||||
|
created_at: 1673428978,
|
||||||
|
kind: 9735,
|
||||||
|
tags: [
|
||||||
|
["p", "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"],
|
||||||
|
["bolt11", "lnbc1u1p3mull3pp5qw4x46ew6kjknudypyjsg8maw935tr5kkuz7t6h7pugp3pt4msyqhp5zp40yd97a028sgr9x4yxq5d57gft6vwja58e8cl0eea4uasr6apscqzpgxqyz5vqsp53m2n8h6yeflgukv5fhwm802kur6un9w8nvycl7auk67w5g2u008q9qyyssqml8rfmxyvp32qd5939qx7uu0w6ppjuujlpwsrz28m9u0dzp799hz5j72w0xm8pg97hd4hdvwh9zxaw2hewnnmzewvc550f9y3qsfaegphmk0mu"],
|
||||||
|
["preimage", "3539663535656537343331663432653165396430623966633664656664646563"],
|
||||||
|
["description", "{\"id\":\"3cf02d7f0ccd9711c25098fc50b3a7ab880326e4e51cc8c7a7b59f147cff4fff\",\"sig\":\"e9e9bb2bac4267a107ab5c3368f504b4f11b8e3b5ae875a1d63c74d6934138d2521dc35815b6f534fc5d803cbf633736d871886368bb8f92c4ad3837a68a06f2\",\"pubkey\":\"730b43e6f62c2ab22710b046e481802c8ac1108ed2cb9c21dff808d57ba24b6c\",\"created_at\":1712487443,\"kind\":9734,\"tags\":[[\"relays\",\"wss://nostr.kosmos.org\",\"wss://relay.example.com\"],[\"p\",\"07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3\"]],\"content\":\"\"}"]
|
||||||
|
],
|
||||||
|
content: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
before do
|
||||||
|
user.save!
|
||||||
|
zap.save!
|
||||||
|
|
||||||
|
allow(NostrManager::CreateZapReceipt).to receive(:call)
|
||||||
|
.and_return(zap_receipt)
|
||||||
|
allow(NostrManager::PublishZapReceipt).to receive(:call)
|
||||||
|
.and_return(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns a 200 status" do
|
||||||
|
post "/webhooks/lndhub", params: payload.to_json
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "adds the settlement date/time to the zap record" do
|
||||||
|
post "/webhooks/lndhub", params: payload.to_json
|
||||||
|
expect(user.zaps.first.settled_at.to_i).to eq(1673428978)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "creates and adds a zap receipt to the zap record" do
|
||||||
|
post "/webhooks/lndhub", params: payload.to_json
|
||||||
|
expect(user.zaps.first.receipt).not_to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "publishes the zap receipt" do
|
||||||
|
expect(NostrManager::PublishZapReceipt).to receive(:call).with(zap: zap)
|
||||||
|
post "/webhooks/lndhub", params: payload.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with notifications disabled for zaps" do
|
||||||
|
before do
|
||||||
|
user.update! preferences: { lightning_notify_zap_received: "disabled" }
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not send a notification" do
|
||||||
|
post "/webhooks/lndhub", params: payload.to_json
|
||||||
|
expect(enqueued_jobs.size).to eq(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,21 +2,21 @@ require 'rails_helper'
|
|||||||
|
|
||||||
RSpec.describe "Well-known URLs", type: :request do
|
RSpec.describe "Well-known URLs", type: :request do
|
||||||
describe "GET /nostr" do
|
describe "GET /nostr" do
|
||||||
context "without username param" do
|
describe "without username param" do
|
||||||
it "returns a 422 status" do
|
it "returns a 422 status" do
|
||||||
get "/.well-known/nostr.json"
|
get "/.well-known/nostr.json"
|
||||||
expect(response).to have_http_status(:unprocessable_entity)
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "non-existent user" do
|
describe "non-existent user" do
|
||||||
it "returns a 404 status" do
|
it "returns a 404 status" do
|
||||||
get "/.well-known/nostr.json?name=bob"
|
get "/.well-known/nostr.json?name=bob"
|
||||||
expect(response).to have_http_status(:not_found)
|
expect(response).to have_http_status(:not_found)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "user does not have a nostr pubkey configured" do
|
describe "user does not have a nostr pubkey configured" do
|
||||||
let(:user) { create :user, cn: 'spongebob', ou: 'kosmos.org' }
|
let(:user) { create :user, cn: 'spongebob', ou: 'kosmos.org' }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
@@ -30,7 +30,7 @@ RSpec.describe "Well-known URLs", type: :request do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "user with nostr pubkey" do
|
describe "user with nostr pubkey" do
|
||||||
let(:user) { create :user, cn: 'bobdylan', ou: 'kosmos.org' }
|
let(:user) { create :user, cn: 'bobdylan', ou: 'kosmos.org' }
|
||||||
before do
|
before do
|
||||||
user.save!
|
user.save!
|
||||||
@@ -45,6 +45,59 @@ RSpec.describe "Well-known URLs", type: :request do
|
|||||||
expect(res["names"].keys.size).to eq(1)
|
expect(res["names"].keys.size).to eq(1)
|
||||||
expect(res["names"]["bobdylan"]).to eq(user.nostr_pubkey)
|
expect(res["names"]["bobdylan"]).to eq(user.nostr_pubkey)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "without relay configured" do
|
||||||
|
it "does not include a recommended relay" do
|
||||||
|
get "/.well-known/nostr.json?name=bobdylan"
|
||||||
|
res = JSON.parse(response.body)
|
||||||
|
expect(res["relays"]).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with relay configured" do
|
||||||
|
before do
|
||||||
|
Setting.nostr_relay_url = "wss://nostr.kosmos.org"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "includes a recommended relay" do
|
||||||
|
get "/.well-known/nostr.json?name=bobdylan"
|
||||||
|
res = JSON.parse(response.body)
|
||||||
|
expect(res["relays"][user.nostr_pubkey].length).to eq(1)
|
||||||
|
expect(res["relays"][user.nostr_pubkey].first).to eq("wss://nostr.kosmos.org")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "placeholder username for domain's own pubkey" do
|
||||||
|
it "returns the configured nostr pubkey" do
|
||||||
|
get "/.well-known/nostr.json?name=_"
|
||||||
|
res = JSON.parse(response.body)
|
||||||
|
expect(res["names"]["_"]).to eq(Setting.nostr_public_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with relay configured" do
|
||||||
|
before do
|
||||||
|
Setting.nostr_relay_url = "wss://nostr.kosmos.org"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns the pubkey and relay" do
|
||||||
|
get "/.well-known/nostr.json?name=_"
|
||||||
|
res = JSON.parse(response.body)
|
||||||
|
expect(res["relays"]["_"].length).to eq(1)
|
||||||
|
expect(res["relays"]["_"].first).to eq("wss://nostr.kosmos.org")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "nostr service integration not enabled" do
|
||||||
|
before do
|
||||||
|
Setting.nostr_enabled = false
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns a 404 status" do
|
||||||
|
get "/.well-known/nostr.json?name=_"
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -53,11 +53,32 @@ RSpec.describe CreateAccount, type: :model do
|
|||||||
|
|
||||||
expect(enqueued_jobs.size).to eq(1)
|
expect(enqueued_jobs.size).to eq(1)
|
||||||
|
|
||||||
args = enqueued_jobs.first['arguments']
|
args = enqueued_jobs.first['arguments'][0]
|
||||||
expect(args[0]).to eq('halfinney')
|
expect(args["username"]).to eq('halfinney')
|
||||||
expect(args[1]).to eq('kosmos.org')
|
expect(args["domain"]).to eq('kosmos.org')
|
||||||
expect(args[2]).to eq('halfinney@example.com')
|
expect(args["email"]).to eq('halfinney@example.com')
|
||||||
expect(args[3]).to match(/^{SSHA512}.{171}=/)
|
expect(args["hashed_pw"]).to match(/^{SSHA512}.{171}=/)
|
||||||
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
clear_enqueued_jobs
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#add_ldap_document for pre-confirmed account" do
|
||||||
|
include ActiveJob::TestHelper
|
||||||
|
|
||||||
|
let(:service) { CreateAccount.new(account: {
|
||||||
|
username: 'halfinney',
|
||||||
|
email: 'halfinney@example.com',
|
||||||
|
password: 'remember-remember-the-5th-of-november',
|
||||||
|
confirmed: true
|
||||||
|
})}
|
||||||
|
|
||||||
|
it "enqueues a job to create the LDAP user document" do
|
||||||
|
service.send(:add_ldap_document)
|
||||||
|
args = enqueued_jobs.first['arguments'][0]
|
||||||
|
expect(args["confirmed"]).to be(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
after do
|
after do
|
||||||
|
|||||||
45
spec/services/nostr_manager/create_zap_receipt_spec.rb
Normal file
45
spec/services/nostr_manager/create_zap_receipt_spec.rb
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe NostrManager::CreateZapReceipt, type: :model do
|
||||||
|
let(:user) { create :user, ln_account: "123456abcdef" }
|
||||||
|
let(:zap) { create :zap, user: user }
|
||||||
|
|
||||||
|
# before do
|
||||||
|
# user.save!
|
||||||
|
# zap.save!
|
||||||
|
# end
|
||||||
|
|
||||||
|
subject {
|
||||||
|
described_class.call(
|
||||||
|
zap: zap, paid_at: 1673428978,
|
||||||
|
preimage: "3539663535656537343331663432653165396430623966633664656664646563"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe "Zap receipt" do
|
||||||
|
it "is a kind:9735 note" do
|
||||||
|
expect(subject).to be_a(Nostr::Event)
|
||||||
|
expect(subject.kind).to eq(9735)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "sets created_at to when the invoice was paid" do
|
||||||
|
expect(subject.created_at).to eq(1673428978)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "contains the zap recipient" do
|
||||||
|
expect(subject.tags.find{|t| t[0] == "p"}[1]).to eq("07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "contains the bolt11 invoice" do
|
||||||
|
expect(subject.tags.find{|t| t[0] == "bolt11"}[1]).to eq(zap.payment_request)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "contains the invoice preimage" do
|
||||||
|
expect(subject.tags.find{|t| t[0] == "preimage"}[1]).to eq("3539663535656537343331663432653165396430623966633664656664646563")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "contains the serialized zap request event as description" do
|
||||||
|
expect(subject.tags.find{|t| t[0] == "description"}[1]).to eq(zap.request_event.to_json)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
53
spec/services/nostr_manager/publish_zap_receipt_spec.rb
Normal file
53
spec/services/nostr_manager/publish_zap_receipt_spec.rb
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe NostrManager::PublishZapReceipt, type: :model do
|
||||||
|
let(:user) { create :user, ln_account: "123456abcdef" }
|
||||||
|
let(:zap) { create :zap, user: user }
|
||||||
|
|
||||||
|
describe "Default/delayed execution" do
|
||||||
|
it "publishes zap receipts to all requested relays" do
|
||||||
|
expect(NostrPublishEventJob).to receive(:perform_later)
|
||||||
|
.exactly(2).times.and_return(true)
|
||||||
|
|
||||||
|
described_class.call(zap: zap)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with a long relay list" do
|
||||||
|
before do
|
||||||
|
relays = zap.request["tags"].find { |t| t.first == "relays" }
|
||||||
|
[
|
||||||
|
"wss://aegonstargaryen.example.com", "wss://visenya.example.com",
|
||||||
|
"wss://rhaenys.example.com", "wss://housevelaryon.example.com",
|
||||||
|
"wss://aemond.example.com", "wss://jaehaerys.example.com",
|
||||||
|
"wss://daenerys.example.com", "wss://corlys.example.com",
|
||||||
|
"wss://laenor.example.com", "wss://alysanne.example.com",
|
||||||
|
"wss://balerion.example.com", "wss://meraxes.example.com",
|
||||||
|
"wss://vhaegar.example.com", "wss://vermax.example.com",
|
||||||
|
"wss://caraxes.example.com"
|
||||||
|
].each do |url|
|
||||||
|
relays << url
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "limits publishing attempts to the first 12 relays" do
|
||||||
|
expect(NostrPublishEventJob).to receive(:perform_later)
|
||||||
|
.exactly(12).times.and_return(true)
|
||||||
|
|
||||||
|
described_class.call(zap: zap)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with own relay configured" do
|
||||||
|
before do
|
||||||
|
Setting.nostr_relay_url = "wss://foobar.kosmos.org"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "also publishes the receipt to our own relay" do
|
||||||
|
expect(NostrPublishEventJob).to receive(:perform_later)
|
||||||
|
.exactly(13).times.and_return(true)
|
||||||
|
|
||||||
|
described_class.call(zap: zap)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
206
spec/services/nostr_manager/verify_zap_request_spec.rb
Normal file
206
spec/services/nostr_manager/verify_zap_request_spec.rb
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe NostrManager::VerifyZapRequest, type: :model do
|
||||||
|
describe "Signature invalid" do
|
||||||
|
let(:event) {
|
||||||
|
Nostr::Event.new(
|
||||||
|
id: "3cf02d7f0ccd9711c25098fc50b3a7ab880326e4e51cc8c7a7b59f147cff4fff",
|
||||||
|
pubkey: "730b43e6f62c2ab22710b046e481802c8ac1108ed2cb9c21dff808d57ba24b6c",
|
||||||
|
created_at: 1712487443,
|
||||||
|
kind: 9734,
|
||||||
|
tags: [
|
||||||
|
["relays", "wss://nostr.kosmos.org", "wss://relay.example.com"],
|
||||||
|
["p", "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"]],
|
||||||
|
content: "",
|
||||||
|
sig: "1234562bac4267a107ab5c3368f504b4f11b8e3b5ae875a1d63c74d6934138d2521dc35815b6f534fc5d803cbf633736d871886368bb8f92c4ad3837a68a06f2"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
subject {
|
||||||
|
NostrManager::VerifyZapRequest.call(amount: 100000, event: event, lnurl: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
it "returns false" do
|
||||||
|
expect(subject).to be(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "p tag missing" do
|
||||||
|
let(:event) {
|
||||||
|
Nostr::Event.new(
|
||||||
|
id: "eb53c5e728b625831e318c7ab09373502dd4a41ed11c17f80f32fd1c3fe1252b",
|
||||||
|
pubkey: "730b43e6f62c2ab22710b046e481802c8ac1108ed2cb9c21dff808d57ba24b6c",
|
||||||
|
created_at: 1712580669,
|
||||||
|
kind: 9734,
|
||||||
|
tags: [["relays", "wss://nostr.kosmos.org","wss://relay.example.com"]],
|
||||||
|
content: "",
|
||||||
|
sig: "8e22dcb3e91d080c9549ea0808b018194cc352dde3056a4911796d6f0239de7e1955f287c7e6769909e59ab0ffa09e307178c32128dc451823fb00102ed7e80a"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
subject {
|
||||||
|
NostrManager::VerifyZapRequest.call(amount: 100000, event: event, lnurl: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
it "returns false" do
|
||||||
|
expect(subject).to be(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "p tag not a valid pubkey" do
|
||||||
|
let(:event) {
|
||||||
|
Nostr::Event.new(
|
||||||
|
id: "f2c23f038cf4d0307147d40ddceb87e34ee94acf632f5a67217a55a0b41c5d95",
|
||||||
|
pubkey: "730b43e6f62c2ab22710b046e481802c8ac1108ed2cb9c21dff808d57ba24b6c",
|
||||||
|
created_at: 1712581702,
|
||||||
|
kind: 9734,
|
||||||
|
tags: [
|
||||||
|
["relays", "wss://nostr.kosmos.org","wss://relay.example.com"],
|
||||||
|
["p","123456abcdef"]
|
||||||
|
],
|
||||||
|
content: "",
|
||||||
|
sig: "b33e6b231273d38629f5efb86325ce3b4e02a7d84dfa1f37167ca2de8392cea654ae34000fc365ddfc6e21dc8ce2365fcb000ccb9064fbbcd54c7ea4a943955b"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
subject {
|
||||||
|
NostrManager::VerifyZapRequest.call(amount: 100000, event: event, lnurl: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
it "returns false" do
|
||||||
|
expect(subject).to be(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Minimum valid request" do
|
||||||
|
let(:event) {
|
||||||
|
Nostr::Event.new(
|
||||||
|
id: "3cf02d7f0ccd9711c25098fc50b3a7ab880326e4e51cc8c7a7b59f147cff4fff",
|
||||||
|
pubkey: "730b43e6f62c2ab22710b046e481802c8ac1108ed2cb9c21dff808d57ba24b6c",
|
||||||
|
created_at: 1712487443,
|
||||||
|
kind: 9734,
|
||||||
|
tags: [
|
||||||
|
["relays", "wss://nostr.kosmos.org", "wss://relay.example.com"],
|
||||||
|
["p", "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"]],
|
||||||
|
content: "",
|
||||||
|
sig: "e9e9bb2bac4267a107ab5c3368f504b4f11b8e3b5ae875a1d63c74d6934138d2521dc35815b6f534fc5d803cbf633736d871886368bb8f92c4ad3837a68a06f2"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
subject {
|
||||||
|
NostrManager::VerifyZapRequest.call(amount: 100000, event: event, lnurl: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
it "returns true" do
|
||||||
|
expect(subject).to be(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Optional amount property" do
|
||||||
|
describe "does not match given amount" do
|
||||||
|
let(:event) {
|
||||||
|
Nostr::Event.new(
|
||||||
|
id: "be3d3ba15a257546f6fbba849d4717641fd4ea9f21ae6e9278a045f31d212c5e",
|
||||||
|
pubkey: "730b43e6f62c2ab22710b046e481802c8ac1108ed2cb9c21dff808d57ba24b6c",
|
||||||
|
created_at: 1712579812,
|
||||||
|
kind: 9734,
|
||||||
|
tags: [
|
||||||
|
["relays", "wss://nostr.kosmos.org","wss://relay.example.com"],
|
||||||
|
["p", "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"],
|
||||||
|
["amount", "21000"]
|
||||||
|
],
|
||||||
|
content: "",
|
||||||
|
sig: "2ca9cfd0fdaf43dd8a50ab586e83da4bd9d592def8ed198536f5e3e7aad3537818687e42d98eb61d60e33dbd848c1eecf72b68fd98376bbabdab7e029e810869"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
subject {
|
||||||
|
NostrManager::VerifyZapRequest.call(amount: 100000, event: event, lnurl: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
it "returns false" do
|
||||||
|
expect(subject).to be(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "matches given amount" do
|
||||||
|
let(:event) {
|
||||||
|
Nostr::Event.new(
|
||||||
|
id: "be3d3ba15a257546f6fbba849d4717641fd4ea9f21ae6e9278a045f31d212c5e",
|
||||||
|
pubkey: "730b43e6f62c2ab22710b046e481802c8ac1108ed2cb9c21dff808d57ba24b6c",
|
||||||
|
created_at: 1712579812,
|
||||||
|
kind: 9734,
|
||||||
|
tags: [
|
||||||
|
["relays", "wss://nostr.kosmos.org","wss://relay.example.com"],
|
||||||
|
["p", "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"],
|
||||||
|
["amount", "21000"]
|
||||||
|
],
|
||||||
|
content: "",
|
||||||
|
sig: "2ca9cfd0fdaf43dd8a50ab586e83da4bd9d592def8ed198536f5e3e7aad3537818687e42d98eb61d60e33dbd848c1eecf72b68fd98376bbabdab7e029e810869"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
subject {
|
||||||
|
NostrManager::VerifyZapRequest.call(amount: 21000, event: event, lnurl: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
it "returns true" do
|
||||||
|
expect(subject).to be(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Optional amount property" do
|
||||||
|
describe "does not match given amount" do
|
||||||
|
let(:event) {
|
||||||
|
Nostr::Event.new(
|
||||||
|
id: "be3d3ba15a257546f6fbba849d4717641fd4ea9f21ae6e9278a045f31d212c5e",
|
||||||
|
pubkey: "730b43e6f62c2ab22710b046e481802c8ac1108ed2cb9c21dff808d57ba24b6c",
|
||||||
|
created_at: 1712579812,
|
||||||
|
kind: 9734,
|
||||||
|
tags: [
|
||||||
|
["relays", "wss://nostr.kosmos.org","wss://relay.example.com"],
|
||||||
|
["p", "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"],
|
||||||
|
["amount", "21000"]
|
||||||
|
],
|
||||||
|
content: "",
|
||||||
|
sig: "2ca9cfd0fdaf43dd8a50ab586e83da4bd9d592def8ed198536f5e3e7aad3537818687e42d98eb61d60e33dbd848c1eecf72b68fd98376bbabdab7e029e810869"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
subject {
|
||||||
|
NostrManager::VerifyZapRequest.call(amount: 100000, event: event, lnurl: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
it "returns false" do
|
||||||
|
expect(subject).to be(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "matches given amount" do
|
||||||
|
let(:event) {
|
||||||
|
Nostr::Event.new(
|
||||||
|
id: "be3d3ba15a257546f6fbba849d4717641fd4ea9f21ae6e9278a045f31d212c5e",
|
||||||
|
pubkey: "730b43e6f62c2ab22710b046e481802c8ac1108ed2cb9c21dff808d57ba24b6c",
|
||||||
|
created_at: 1712579812,
|
||||||
|
kind: 9734,
|
||||||
|
tags: [
|
||||||
|
["relays", "wss://nostr.kosmos.org","wss://relay.example.com"],
|
||||||
|
["p", "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"],
|
||||||
|
["amount", "21000"]
|
||||||
|
],
|
||||||
|
content: "",
|
||||||
|
sig: "2ca9cfd0fdaf43dd8a50ab586e83da4bd9d592def8ed198536f5e3e7aad3537818687e42d98eb61d60e33dbd848c1eecf72b68fd98376bbabdab7e029e810869"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
subject {
|
||||||
|
NostrManager::VerifyZapRequest.call(amount: 21000, event: event, lnurl: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
it "returns true" do
|
||||||
|
expect(subject).to be(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user