Compare commits
92 Commits
d6d09b57b8
...
feature/no
| Author | SHA1 | Date | |
|---|---|---|---|
|
9866cd0404
|
|||
|
10d29b6fab
|
|||
|
6f8f60a9e2
|
|||
|
c1b4665706
|
|||
|
5447150d4d
|
|||
|
bf26703b2d
|
|||
|
21c6264ea9
|
|||
|
79ef9fa6d5
|
|||
|
04a9061663
|
|||
|
5283f6fce7
|
|||
|
a08a4746f7
|
|||
|
9e3652479b
|
|||
|
011386fb8d
|
|||
|
4d77f5d38c
|
|||
|
64de4deddd
|
|||
|
8f7994d82e
|
|||
|
a7d0e71ab6
|
|||
|
27d9f73c61
|
|||
|
ed3de8b16f
|
|||
|
d7b4c67953
|
|||
| 7489d4a32f | |||
|
ac77e5b7c1
|
|||
|
e544c28105
|
|||
|
4909dac5c2
|
|||
| 3cf4348695 | |||
|
af3da0a26c
|
|||
|
2d32320c7d
|
|||
|
fc2bec6246
|
|||
|
5addd25186
|
|||
|
215d178e69
|
|||
|
5474bf66e7
|
|||
|
ef2a37e2bf
|
|||
|
0e3180602c
|
|||
|
15e2f9b962
|
|||
|
4ae10c9b53
|
|||
| 45137e0cfe | |||
|
717fe93104
|
|||
|
fdac789ccb
|
|||
|
9355dab6b6
|
|||
|
f3676949d2
|
|||
|
79952b73c5
|
|||
| 17c419403e | |||
|
6d06312a5c
|
|||
|
acb399b0b7
|
|||
|
bf20b6467e
|
|||
|
b91d90d75c
|
|||
|
3284bbf6ca
|
|||
|
171b84ee81
|
|||
|
54b01dd282
|
|||
|
e08ea64f47
|
|||
|
8cc2c9554f
|
|||
| 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 | |||
|
1685d6ecf8
|
|||
|
5348a229a6
|
|||
|
bad3b7a2be
|
|||
|
b541e95bb7
|
|||
|
3f43fe8101
|
@@ -29,6 +29,7 @@
|
||||
|
||||
#
|
||||
# Service Integrations
|
||||
# (sorted alphabetically by service name)
|
||||
#
|
||||
|
||||
# BTCPAY_PUBLIC_URL='https://btcpay.example.com'
|
||||
@@ -62,5 +63,9 @@
|
||||
|
||||
# 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_REDIS_URL='redis://localhost:6379/2'
|
||||
|
||||
13
Dockerfile
13
Dockerfile
@@ -1,18 +1,11 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM debian:bullseye-slim as base
|
||||
FROM ruby:3.3.4
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
# TODO Remove when upstream Ruby works properly on Apple silicon
|
||||
RUN apt update && apt install -y build-essential wget autoconf libpq-dev pkg-config
|
||||
RUN wget https://github.com/postmodern/ruby-install/releases/download/v0.9.3/ruby-install-0.9.3.tar.gz \
|
||||
&& tar -xzvf ruby-install-0.9.3.tar.gz \
|
||||
&& cd ruby-install-0.9.3/ \
|
||||
&& make install
|
||||
RUN ruby-install -p https://github.com/ruby/ruby/pull/9371.diff ruby 3.3.0
|
||||
ENV PATH="/opt/rubies/ruby-3.3.0/bin:${PATH}"
|
||||
RUN apt-get update -qq && apt-get install -y --no-install-recommends curl \
|
||||
ldap-utils tini libvips
|
||||
|
||||
RUN apt-get install -y --no-install-recommends curl ldap-utils tini libvips
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
|
||||
RUN apt-get update && apt-get install -y nodejs
|
||||
|
||||
|
||||
2
Gemfile
2
Gemfile
@@ -61,7 +61,7 @@ gem "sentry-rails"
|
||||
# Services
|
||||
gem 'discourse_api'
|
||||
gem "lnurl"
|
||||
gem 'manifique'
|
||||
gem 'manifique', '~> 1.1.0'
|
||||
gem 'nostr', '~> 0.6.0'
|
||||
|
||||
group :development, :test do
|
||||
|
||||
@@ -245,7 +245,7 @@ GEM
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
manifique (1.0.1)
|
||||
manifique (1.1.0)
|
||||
faraday (~> 2.9.0)
|
||||
faraday-follow_redirects (= 0.3.0)
|
||||
nokogiri (~> 1.16.0)
|
||||
@@ -515,7 +515,7 @@ DEPENDENCIES
|
||||
listen (~> 3.2)
|
||||
lnurl
|
||||
lockbox
|
||||
manifique
|
||||
manifique (~> 1.1.0)
|
||||
net-ldap
|
||||
nostr (~> 0.6.0)
|
||||
pagy (~> 6.0, >= 6.0.2)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@layer components {
|
||||
.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 %>
|
||||
<%= method("#{@type}_field").call :setting, @key,
|
||||
value: Setting.public_send(@key),
|
||||
placeholder: @placeholder,
|
||||
data: {
|
||||
:'default-value' => Setting.get_field(@key)[:default]
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
module FormElements
|
||||
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
|
||||
@positioning = :vertical
|
||||
@title = title
|
||||
@@ -10,6 +10,7 @@ module FormElements
|
||||
@key = key.to_sym
|
||||
@type = type
|
||||
@resettable = is_resettable?(@key)
|
||||
@placeholder = placeholder
|
||||
end
|
||||
|
||||
def is_resettable?(key)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="flex flex-col">
|
||||
<label class="font-bold mb-1"><%= @title %></label>
|
||||
<% if @description.present? %>
|
||||
<p class="text-gray-500"><%= @descripton %></p>
|
||||
<p class="text-gray-500"><%= @description %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="relative ml-4 inline-flex flex-shrink-0">
|
||||
|
||||
@@ -12,7 +12,7 @@ module FormElements
|
||||
@enabled = enabled
|
||||
@input_enabled = input_enabled
|
||||
@title = title
|
||||
@descripton = description
|
||||
@description = description
|
||||
@button_text = @enabled ? "Switch off" : "Switch on"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<div class="w-[72vw] md:w-[500px]">
|
||||
<header class="absolute z-10 h-36 sm:h-44 inset-x-1 top-1 rounded-t
|
||||
bg-cover bg-center bg-gray-50"
|
||||
style="background-image: url('<%= @profile["banner"]%>');">
|
||||
<div class="inline-block z-20 size-28 sm:size-32 ml-4 mt-16 sm:mt-20">
|
||||
<% if @profile["picture"].present? %>
|
||||
<img src="<%= @profile["picture"] %>"
|
||||
class="inline-block size:28 sm:size-32 rounded-full border-2 border-white" />
|
||||
<% else %>
|
||||
<span class="inline-block size:28 sm:size-32 overflow-hidden rounded-full border-2 border-white bg-gray-100">
|
||||
<svg class="size-full text-gray-300" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</header>
|
||||
<main class="mt-44 sm:mt-52">
|
||||
<%= form_for(@user, url: setting_path(:nostr), html: { :method => :put }) do |f| %>
|
||||
<%= render FormElements::FieldsetComponent.new(tag: "div", title: "Display name") do %>
|
||||
<%= f.text_field :display_name, value: @display_name, class: "w-full sm:w-3/5" %>
|
||||
<% if @validation_errors.present? && @validation_errors[:display_name].present? %>
|
||||
<p class="error-msg mt-2"><%= @validation_errors[:display_name].first %></p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= render FormElements::FieldsetComponent.new(tag: "div", title: "Nostr address (NIP-05)") do %>
|
||||
<%= f.text_field :nip05_address, value: @profile["nip05"], class: "w-full sm:w-3/5" %>
|
||||
<% if @validation_errors.present? && @validation_errors[:nip05_address].present? %>
|
||||
<p class="error-msg mt-2"><%= @validation_errors[:nip05_address].first %></p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= render FormElements::FieldsetComponent.new(tag: "div", title: "Ligtning address for Zaps") do %>
|
||||
<%= f.text_field :lud16_address, value: @profile["lud16"], class: "w-full sm:w-3/5" %>
|
||||
<% if @validation_errors.present? && @validation_errors[:lud16_address].present? %>
|
||||
<p class="error-msg mt-2"><%= @validation_errors[:lud16_address].first %></p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</main>
|
||||
<footer>
|
||||
<%# <%= @profile.inspect %>
|
||||
<%# <%= @profile_event.inspect %>
|
||||
</footer>
|
||||
</div>
|
||||
28
app/components/settings/nostr_edit_profile_component.rb
Normal file
28
app/components/settings/nostr_edit_profile_component.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Settings
|
||||
class NostrEditProfileComponent < ViewComponent::Base
|
||||
def initialize(user:, profile_event:)
|
||||
if profile_event.present?
|
||||
@user = user
|
||||
@profile_event = profile_event
|
||||
@profile = JSON.parse(profile_event["content"])
|
||||
@display_name = @profile["display_name"] || @profile["displayName"]
|
||||
|
||||
if @profile["nip05"].present? && @profile["nip05"] == @user.address
|
||||
# "Your profile's Nostr address is set to <strong>#{ user_address }</strong>"
|
||||
else
|
||||
# "Your profile's Nostr address is not set to <strong>#{ user_address }</strong> yet"
|
||||
end
|
||||
|
||||
if @profile["lud16"].present? && @profile["lud16"] == @user.address
|
||||
# "Your profile's Lightning address is set to <strong>#{ user_address }</strong>"
|
||||
else
|
||||
# "Your profile's Lightning address is not set to <strong>#{ user_address }</strong> yet"
|
||||
end
|
||||
else
|
||||
# "We could not find a profile for your public key"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
<% @statuses.each do |status| %>
|
||||
<%= render StatusTextComponent.new(
|
||||
text: status[:text],
|
||||
icon_name: status[:icon_name],
|
||||
icon_color: status[:icon_color]
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<% if @status == 1 %>
|
||||
<p class="mt-8">
|
||||
<button class="btn-md btn-blue">
|
||||
Edit my profile
|
||||
</button>
|
||||
</p>
|
||||
<% elsif @status == 2 %>
|
||||
<p class="mt-8">
|
||||
<button class="btn-md btn-blue">
|
||||
Create my profile
|
||||
</button>
|
||||
</p>
|
||||
<% end %>
|
||||
53
app/components/settings/nostr_profile_status_component.rb
Normal file
53
app/components/settings/nostr_profile_status_component.rb
Normal file
@@ -0,0 +1,53 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Settings
|
||||
class NostrProfileStatusComponent < ViewComponent::Base
|
||||
def initialize(profile_event:, user_address:)
|
||||
@statuses = []
|
||||
|
||||
if profile_event.present?
|
||||
profile = JSON.parse(profile_event["content"])
|
||||
|
||||
@statuses.push({
|
||||
text: "You have a public Nostr profile",
|
||||
icon_name: "check-circle",
|
||||
icon_color: "emerald-500"
|
||||
})
|
||||
|
||||
if profile["nip05"].present? && profile["nip05"] == user_address
|
||||
@statuses.push({
|
||||
text: "Your profile's Nostr address is set to <strong>#{ user_address }</strong>",
|
||||
icon_name: "check-circle",
|
||||
icon_color: "emerald-500"
|
||||
})
|
||||
else
|
||||
@statuses.push({
|
||||
text: "Your profile's Nostr address is not set to <strong>#{ user_address }</strong> yet",
|
||||
icon_name: "alert-octagon",
|
||||
icon_color: "amber-500"
|
||||
})
|
||||
end
|
||||
|
||||
if profile["lud16"].present? && profile["lud16"] == user_address
|
||||
@statuses.push({
|
||||
text: "Your profile's Lightning address is set to <strong>#{ user_address }</strong>",
|
||||
icon_name: "check-circle",
|
||||
icon_color: "emerald-500"
|
||||
})
|
||||
else
|
||||
@statuses.push({
|
||||
text: "Your profile's Lightning address is not set to <strong>#{ user_address }</strong> yet",
|
||||
icon_name: "alert-octagon",
|
||||
icon_color: "amber-500"
|
||||
})
|
||||
end
|
||||
else
|
||||
@statuses.push({
|
||||
text: "We could not find a profile for your public key",
|
||||
icon_name: "alert-octagon",
|
||||
icon_color: "amber-500"
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
<%= render StatusTextComponent.new(
|
||||
text: @text,
|
||||
icon_name: @icon_name,
|
||||
icon_color: @icon_color) %>
|
||||
|
||||
<% if @status == 1 %>
|
||||
<p class="mt-8">
|
||||
<button class="btn-md btn-blue">
|
||||
Add the relay to my list
|
||||
</button>
|
||||
</p>
|
||||
<% elsif @status == 2 %>
|
||||
<p class="mt-8">
|
||||
<button class="btn-md btn-blue">
|
||||
Set up default relays
|
||||
</button>
|
||||
</p>
|
||||
<% end %>
|
||||
34
app/components/settings/nostr_relay_status_component.rb
Normal file
34
app/components/settings/nostr_relay_status_component.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Settings
|
||||
class NostrRelayStatusComponent < ViewComponent::Base
|
||||
def initialize(nip65_event:)
|
||||
if nip65_event.present?
|
||||
if relay_urls(nip65_event).any? { |r| r.include?("wss://nostr.kosmos.org") }
|
||||
@text = "You have a relay list, and the Kosmos relay is part of it"
|
||||
@icon_name = "check-circle"
|
||||
@icon_color = "emerald-500"
|
||||
@status = 0
|
||||
else
|
||||
@text = "The Kosmos relay is missing from your relay list"
|
||||
@icon_name = "alert-octagon"
|
||||
@icon_color = "amber-500"
|
||||
@status = 1
|
||||
end
|
||||
else
|
||||
@text = "We could not find a relay list for your public key"
|
||||
@icon_name = "alert-octagon"
|
||||
@icon_color = "amber-500"
|
||||
@status = 2
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def relay_urls(nip65_event)
|
||||
nip65_event["tags"].select{ |t| t[0] == "r" }.map{ |t| t[1] }
|
||||
# @inbox_relay_urls = relay_tags&.select{ |t| t[2] == "read" }&.map{ |t| t[1] }
|
||||
# @outbox_relay_urls = relay_tags&.select{ |t| t[2] != "read" }&.map{ |t| t[1] }
|
||||
end
|
||||
end
|
||||
end
|
||||
8
app/components/status_text_component.html.erb
Normal file
8
app/components/status_text_component.html.erb
Normal file
@@ -0,0 +1,8 @@
|
||||
<p class="flex gap-x-4 items-center">
|
||||
<span class="inline-block h-6 w-6 grow-0 text-<%= @icon_color %>">
|
||||
<%= render "icons/#{@icon_name}" %>
|
||||
</span>
|
||||
<span>
|
||||
<%= raw @text %>
|
||||
</span>
|
||||
</p>
|
||||
7
app/components/status_text_component.rb
Normal file
7
app/components/status_text_component.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
class StatusTextComponent < ViewComponent::Base
|
||||
def initialize(text:, icon_name:, icon_color:)
|
||||
@text = text
|
||||
@icon_name = icon_name
|
||||
@icon_color = icon_color
|
||||
end
|
||||
end
|
||||
@@ -9,4 +9,12 @@ class Admin::Settings::RegistrationsController < Admin::SettingsController
|
||||
success: "Settings saved"
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def setting_params
|
||||
params.require(:setting).permit([
|
||||
:reserved_usernames, default_services: []
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,11 +9,12 @@ class Admin::SettingsController < Admin::BaseController
|
||||
changed_keys = []
|
||||
|
||||
setting_params.keys.each do |key|
|
||||
next if setting_params[key].nil? ||
|
||||
(Setting.send(key).to_s == setting_params[key].strip)
|
||||
next if clean_param(key).nil? ||
|
||||
(Setting.send(key).to_s == clean_param(key))
|
||||
|
||||
changed_keys.push(key)
|
||||
setting = Setting.new(var: key)
|
||||
setting.value = setting_params[key].strip
|
||||
setting.value = clean_param(key)
|
||||
unless setting.valid?
|
||||
@errors.merge!(setting.errors)
|
||||
end
|
||||
@@ -24,7 +25,7 @@ class Admin::SettingsController < Admin::BaseController
|
||||
end
|
||||
|
||||
changed_keys.each do |key|
|
||||
Setting.send("#{key}=", setting_params[key].strip)
|
||||
Setting.send("#{key}=", clean_param(key))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -37,4 +38,12 @@ class Admin::SettingsController < Admin::BaseController
|
||||
def setting_params
|
||||
params.require(:setting).permit(Setting.editable_keys.map(&:to_sym))
|
||||
end
|
||||
|
||||
def clean_param(key)
|
||||
if Setting.get_field(key)[:type] == :string
|
||||
setting_params[key].strip
|
||||
else
|
||||
setting_params[key]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class LnurlpayController < ApplicationController
|
||||
before_action :check_service_available
|
||||
before_action :find_user
|
||||
before_action :set_cors_access_control_headers, only: [:invoice]
|
||||
before_action :set_cors_access_control_headers
|
||||
|
||||
MIN_SATS = 10
|
||||
MAX_SATS = 1_000_000
|
||||
|
||||
@@ -3,7 +3,7 @@ class Services::ChatController < Services::BaseController
|
||||
before_action :require_service_available
|
||||
|
||||
def show
|
||||
@service_enabled = current_user.service_enabled?(:xmpp)
|
||||
@service_enabled = current_user.service_enabled?(:ejabberd)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -8,8 +8,7 @@ class Services::RemotestorageController < Services::BaseController
|
||||
# unless current_user.service_enabled?(:remotestorage)
|
||||
# redirect_to service_remotestorage_info_path
|
||||
# end
|
||||
@rs_auths = current_user.remote_storage_authorizations
|
||||
# TODO sort by app name
|
||||
# @rs_apps_connected = current_user.remote_storage_authorizations.any?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -3,13 +3,18 @@ class Services::RsAuthsController < Services::BaseController
|
||||
before_action :require_feature_enabled
|
||||
before_action :require_service_available
|
||||
# before_action :require_service_enabled
|
||||
before_action :find_rs_auth
|
||||
before_action :find_rs_auth, only: [:destroy, :launch_app]
|
||||
|
||||
def index
|
||||
@rs_auths = current_user.remote_storage_authorizations
|
||||
# TODO sort by app name?
|
||||
end
|
||||
|
||||
def destroy
|
||||
@auth.destroy!
|
||||
|
||||
respond_to do |format|
|
||||
format.html do redirect_to services_storage_url, flash: {
|
||||
format.html do redirect_to apps_services_storage_url, flash: {
|
||||
success: 'App authorization revoked'
|
||||
}
|
||||
end
|
||||
|
||||
@@ -4,15 +4,24 @@ require "bcrypt"
|
||||
class SettingsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_main_nav_section
|
||||
before_action :set_settings_section, only: [:show, :update, :update_email, :reset_email_password]
|
||||
before_action :set_user, only: [:show, :update, :update_email, :reset_email_password]
|
||||
before_action :set_settings_section, only: [
|
||||
:show, :update, :update_email, :reset_email_password
|
||||
]
|
||||
before_action :set_user, only: [
|
||||
:show, :update, :update_email, :reset_email_password,
|
||||
:fetch_nostr_user_metadata
|
||||
]
|
||||
|
||||
def index
|
||||
redirect_to setting_path(:profile)
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
end
|
||||
@@ -124,6 +133,28 @@ class SettingsController < ApplicationController
|
||||
}
|
||||
end
|
||||
|
||||
def fetch_nostr_user_metadata
|
||||
if @user.nostr_pubkey.present?
|
||||
outbox_relay_urls = nil
|
||||
|
||||
# if @nip65_event = NostrManager::DiscoverUserRelays.call(pubkey: @user.nostr_pubkey)
|
||||
# relay_tags = @nip65_event["tags"].select{ |t| t[0] == "r" }
|
||||
# outbox_relay_urls = relay_tags&.select{ |t| t[2] != "read" }&.map{ |t| t[1] }
|
||||
# end
|
||||
|
||||
# @profile = NostrManager::DiscoverUserProfile.call(
|
||||
# pubkey: @user.nostr_pubkey,
|
||||
# relays: outbox_relay_urls
|
||||
# )
|
||||
@profile = {"content"=>"{\"name\":\"jimmy\",\"picture\":\"https://storage.kosmos.org/jimmy/public/shares/241028-1117-tony.jpg\",\"banner\":\"https://storage.kosmos.org/raucao/public/shares/240604-1517-1500x500.jpg\",\"nip05\":\"jimmy@kosmos.org\",\"lud16\":\"jimmy@kosmos.org\",\"pubkey\":\"07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3\",\"display_name\":\"Jimmy\",\"displayName\":\"Jimmy\",\"about\":\"I don't exist. Follow at your own peril.\"}", "created_at"=>1730114246, "id"=>"6b15b1308a61ee837bd3b50319978314650e435891c259f4ea499f819f35a4f6", "kind"=>0, "pubkey"=>"07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3", "sig"=>"4f681f4b95646bbf88a6eae9ca92c0f2ce5effecfa017556a23490f91a99243aedf81d956ee2466ed64fecb9a03b6b89cd80ff116df0178830977e203867d7ae", "tags"=>[]}
|
||||
# @profile = {"content"=>"{\"name\":\"jimmy\",\"nip05\":\"jimmy@kosmos.org\",\"lud16\":\"jimmy@kosmos.org\",\"pubkey\":\"07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3\",\"display_name\":\"Jimmy\",\"displayName\":\"Jimmy\",\"about\":\"I don't exist. Follow at your own peril.\"}", "created_at"=>1730114246, "id"=>"6b15b1308a61ee837bd3b50319978314650e435891c259f4ea499f819f35a4f6", "kind"=>0, "pubkey"=>"07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3", "sig"=>"4f681f4b95646bbf88a6eae9ca92c0f2ce5effecfa017556a23490f91a99243aedf81d956ee2466ed64fecb9a03b6b89cd80ff116df0178830977e203867d7ae", "tags"=>[]}
|
||||
else
|
||||
@relays, @profile = [nil, nil]
|
||||
end
|
||||
|
||||
render partial: 'nostr_user_metadata'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_main_nav_section
|
||||
@@ -147,11 +178,9 @@ class SettingsController < ApplicationController
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:display_name, :avatar, preferences: [
|
||||
:lightning_notify_sats_received,
|
||||
:remotestorage_notify_auth_created,
|
||||
:xmpp_exchange_contacts_with_invitees
|
||||
])
|
||||
params.require(:user).permit(
|
||||
:display_name, :avatar, preferences: UserPreferences.pref_keys
|
||||
)
|
||||
end
|
||||
|
||||
def email_params
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
class WebfingerController < ApplicationController
|
||||
class WebfingerController < WellKnownController
|
||||
before_action :allow_cross_origin_requests, only: [:show]
|
||||
|
||||
layout false
|
||||
|
||||
def show
|
||||
resource = params[:resource]
|
||||
|
||||
@@ -76,7 +74,7 @@ class WebfingerController < ApplicationController
|
||||
end
|
||||
|
||||
def remotestorage_link
|
||||
auth_url = new_rs_oauth_url(@username)
|
||||
auth_url = new_rs_oauth_url(@username, host: Setting.accounts_domain)
|
||||
storage_url = "#{Setting.rs_storage_url}/#{@username}"
|
||||
|
||||
{
|
||||
@@ -91,10 +89,4 @@ class WebfingerController < ApplicationController
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def allow_cross_origin_requests
|
||||
return unless Rails.env.development?
|
||||
headers['Access-Control-Allow-Origin'] = "*"
|
||||
headers['Access-Control-Allow-Methods'] = "GET"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,14 +7,15 @@ class WebhooksController < ApplicationController
|
||||
def lndhub
|
||||
@user = User.find_by!(ln_account: @payload[:user_login])
|
||||
|
||||
if zap = @user.zaps.find_by(payment_request: @payload[:payment_request])
|
||||
if @zap = @user.zaps.find_by(payment_request: @payload[:payment_request])
|
||||
settled_at = Time.parse(@payload[:settled_at])
|
||||
zap_receipt = NostrManager::CreateZapReceipt.call(
|
||||
zap: zap,
|
||||
paid_at: Time.parse(@payload[:settled_at]).to_i,
|
||||
zap: @zap,
|
||||
paid_at: settled_at.to_i,
|
||||
preimage: @payload[:preimage]
|
||||
)
|
||||
zap.update! receipt: zap_receipt.to_h
|
||||
NostrManager::PublishZapReceipt.call(zap: zap)
|
||||
@zap.update! settled_at: settled_at, receipt: zap_receipt.to_h
|
||||
NostrManager::PublishZapReceipt.call(zap: @zap)
|
||||
end
|
||||
|
||||
send_notifications
|
||||
@@ -41,7 +42,16 @@ class WebhooksController < ApplicationController
|
||||
end
|
||||
|
||||
def send_notifications
|
||||
case @user.preferences[:lightning_notify_sats_received]
|
||||
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"
|
||||
|
||||
@@ -1,16 +1,47 @@
|
||||
class WellKnownController < ApplicationController
|
||||
before_action :require_nostr_enabled, only: [ :nostr ]
|
||||
before_action :allow_cross_origin_requests, only: [ :nostr ]
|
||||
|
||||
layout false
|
||||
|
||||
def nostr
|
||||
http_status :unprocessable_entity and return if params[:name].blank?
|
||||
domain = request.headers["X-Forwarded-Host"].presence || Setting.primary_domain
|
||||
@user = User.where(cn: params[:name], ou: domain).first
|
||||
http_status :not_found and return if @user.nil? || @user.nostr_pubkey.blank?
|
||||
relay_url = Setting.nostr_relay_url.presence
|
||||
|
||||
if params[:name] == "_"
|
||||
if domain == Setting.primary_domain
|
||||
# pubkey for the primary domain without a username (e.g. kosmos.org)
|
||||
res = { names: { "_": Setting.nostr_public_key_primary_domain.presence || Setting.nostr_public_key } }
|
||||
else
|
||||
# pubkey for the akkounts domain without a username (e.g. accounts.kosmos.org)
|
||||
res = { names: { "_": Setting.nostr_public_key } }
|
||||
end
|
||||
|
||||
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|
|
||||
format.json do
|
||||
render json: {
|
||||
names: { "#{@user.cn}": @user.nostr_pubkey }
|
||||
}.to_json
|
||||
render json: res.to_json
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def require_nostr_enabled
|
||||
http_status :not_found unless Setting.nostr_enabled?
|
||||
end
|
||||
|
||||
def allow_cross_origin_requests
|
||||
headers['Access-Control-Allow-Origin'] = "*"
|
||||
headers['Access-Control-Allow-Methods'] = "GET"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
module DashboardHelper
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
module DonationsHelper
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
module InvitationsHelper
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
module LnurlpayHelper
|
||||
end
|
||||
12
app/helpers/services_helper.rb
Normal file
12
app/helpers/services_helper.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
module ServicesHelper
|
||||
|
||||
def service_human_name(key, category = :external)
|
||||
SERVICES[category][key][:name] || key.to_s
|
||||
end
|
||||
|
||||
def service_display_name(key, category = :external)
|
||||
SERVICES[category][key][:display_name] ||
|
||||
service_human_name(key, category)
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
module SettingsHelper
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
module SignupHelper
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
module UsersHelper
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
module WalletHelper
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
module WelcomeHelper
|
||||
end
|
||||
@@ -2,8 +2,8 @@ class XmppExchangeContactsJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(inviter, invitee)
|
||||
return unless inviter.service_enabled?(:xmpp) &&
|
||||
invitee.service_enabled?(:xmpp) &&
|
||||
return unless inviter.service_enabled?(:ejabberd) &&
|
||||
invitee.service_enabled?(:ejabberd) &&
|
||||
inviter.preferences[:xmpp_exchange_contacts_with_invitees]
|
||||
|
||||
ejabberd = EjabberdApiClient.new
|
||||
|
||||
24
app/models/concerns/settings/btcpay_settings.rb
Normal file
24
app/models/concerns/settings/btcpay_settings.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
module Settings
|
||||
module BtcpaySettings
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
field :btcpay_api_url, type: :string,
|
||||
default: ENV["BTCPAY_API_URL"].presence
|
||||
|
||||
field :btcpay_enabled, type: :boolean,
|
||||
default: ENV["BTCPAY_API_URL"].present?
|
||||
|
||||
field :btcpay_public_url, type: :string,
|
||||
default: ENV["BTCPAY_PUBLIC_URL"].presence
|
||||
|
||||
field :btcpay_store_id, type: :string,
|
||||
default: ENV["BTCPAY_STORE_ID"].presence
|
||||
|
||||
field :btcpay_auth_token, type: :string,
|
||||
default: ENV["BTCPAY_AUTH_TOKEN"].presence
|
||||
|
||||
field :btcpay_publish_wallet_balances, type: :boolean, default: true
|
||||
end
|
||||
end
|
||||
end
|
||||
16
app/models/concerns/settings/discourse_settings.rb
Normal file
16
app/models/concerns/settings/discourse_settings.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
module Settings
|
||||
module DiscourseSettings
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
field :discourse_public_url, type: :string,
|
||||
default: ENV["DISCOURSE_PUBLIC_URL"].presence
|
||||
|
||||
field :discourse_enabled, type: :boolean,
|
||||
default: ENV["DISCOURSE_PUBLIC_URL"].present?
|
||||
|
||||
field :discourse_connect_secret, type: :string,
|
||||
default: ENV["DISCOURSE_CONNECT_SECRET"].presence
|
||||
end
|
||||
end
|
||||
end
|
||||
13
app/models/concerns/settings/drone_ci_settings.rb
Normal file
13
app/models/concerns/settings/drone_ci_settings.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
module Settings
|
||||
module DroneCiSettings
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
field :droneci_public_url, type: :string,
|
||||
default: ENV["DRONECI_PUBLIC_URL"].presence
|
||||
|
||||
field :droneci_enabled, type: :boolean,
|
||||
default: ENV["DRONECI_PUBLIC_URL"].present?
|
||||
end
|
||||
end
|
||||
end
|
||||
19
app/models/concerns/settings/ejabberd_settings.rb
Normal file
19
app/models/concerns/settings/ejabberd_settings.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
module Settings
|
||||
module EjabberdSettings
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
field :ejabberd_enabled, type: :boolean,
|
||||
default: ENV["EJABBERD_API_URL"].present?
|
||||
|
||||
field :ejabberd_api_url, type: :string,
|
||||
default: ENV["EJABBERD_API_URL"].presence
|
||||
|
||||
field :ejabberd_admin_url, type: :string,
|
||||
default: ENV["EJABBERD_ADMIN_URL"].presence
|
||||
|
||||
field :ejabberd_buddy_roster, type: :string,
|
||||
default: "Buddies"
|
||||
end
|
||||
end
|
||||
end
|
||||
28
app/models/concerns/settings/email_settings.rb
Normal file
28
app/models/concerns/settings/email_settings.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
module Settings
|
||||
module EmailSettings
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
field :email_enabled, type: :boolean,
|
||||
default: ENV["EMAIL_SMTP_HOST"].present?
|
||||
|
||||
# field :email_smtp_host, type: :string,
|
||||
# default: ENV["EMAIL_SMTP_HOST"].presence
|
||||
#
|
||||
# field :email_smtp_port, type: :string,
|
||||
# default: ENV["EMAIL_SMTP_PORT"].presence || 587
|
||||
#
|
||||
# field :email_smtp_enable_starttls, type: :string,
|
||||
# default: ENV["EMAIL_SMTP_PORT"].presence || true
|
||||
#
|
||||
# field :email_auth_method, type: :string,
|
||||
# default: ENV["EMAIL_AUTH_METHOD"].presence || "plain"
|
||||
#
|
||||
# field :email_imap_host, type: :string,
|
||||
# default: ENV["EMAIL_IMAP_HOST"].presence
|
||||
#
|
||||
# field :email_imap_port, type: :string,
|
||||
# default: ENV["EMAIL_IMAP_PORT"].presence || 993
|
||||
end
|
||||
end
|
||||
end
|
||||
34
app/models/concerns/settings/general_settings.rb
Normal file
34
app/models/concerns/settings/general_settings.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
module Settings
|
||||
module GeneralSettings
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
field :primary_domain, type: :string,
|
||||
default: ENV["PRIMARY_DOMAIN"].presence
|
||||
|
||||
field :accounts_domain, type: :string,
|
||||
default: ENV["AKKOUNTS_DOMAIN"].presence
|
||||
|
||||
#
|
||||
# Internal services
|
||||
#
|
||||
|
||||
field :redis_url, type: :string,
|
||||
default: ENV["REDIS_URL"] || "redis://localhost:6379/0"
|
||||
|
||||
field :s3_enabled, type: :boolean,
|
||||
default: ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false"
|
||||
|
||||
field :sentry_enabled, type: :boolean, readonly: true,
|
||||
default: ENV["SENTRY_DSN"].present?
|
||||
|
||||
#
|
||||
# Registrations
|
||||
#
|
||||
|
||||
field :reserved_usernames, type: :array, default: %w[
|
||||
account accounts donations mail webmaster support
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
13
app/models/concerns/settings/gitea_settings.rb
Normal file
13
app/models/concerns/settings/gitea_settings.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
module Settings
|
||||
module GiteaSettings
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
field :gitea_public_url, type: :string,
|
||||
default: ENV["GITEA_PUBLIC_URL"].presence
|
||||
|
||||
field :gitea_enabled, type: :boolean,
|
||||
default: ENV["GITEA_PUBLIC_URL"].present?
|
||||
end
|
||||
end
|
||||
end
|
||||
25
app/models/concerns/settings/lightning_network_settings.rb
Normal file
25
app/models/concerns/settings/lightning_network_settings.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
module Settings
|
||||
module LightningNetworkSettings
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
field :lndhub_api_url, type: :string,
|
||||
default: ENV["LNDHUB_API_URL"].presence
|
||||
|
||||
field :lndhub_enabled, type: :boolean,
|
||||
default: ENV["LNDHUB_API_URL"].present?
|
||||
|
||||
field :lndhub_admin_token, type: :string,
|
||||
default: ENV["LNDHUB_ADMIN_TOKEN"].presence
|
||||
|
||||
field :lndhub_admin_enabled, type: :boolean,
|
||||
default: ENV["LNDHUB_ADMIN_UI"] || false
|
||||
|
||||
field :lndhub_public_key, type: :string,
|
||||
default: (ENV["LNDHUB_PUBLIC_KEY"] || "")
|
||||
|
||||
field :lndhub_keysend_enabled, type: :boolean,
|
||||
default: -> { self.lndhub_public_key.present? }
|
||||
end
|
||||
end
|
||||
end
|
||||
16
app/models/concerns/settings/mastodon_settings.rb
Normal file
16
app/models/concerns/settings/mastodon_settings.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
module Settings
|
||||
module MastodonSettings
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
field :mastodon_public_url, type: :string,
|
||||
default: ENV["MASTODON_PUBLIC_URL"].presence
|
||||
|
||||
field :mastodon_enabled, type: :boolean,
|
||||
default: ENV["MASTODON_PUBLIC_URL"].present?
|
||||
|
||||
field :mastodon_address_domain, type: :string,
|
||||
default: ENV["MASTODON_ADDRESS_DOMAIN"].presence || self.primary_domain
|
||||
end
|
||||
end
|
||||
end
|
||||
13
app/models/concerns/settings/media_wiki_settings.rb
Normal file
13
app/models/concerns/settings/media_wiki_settings.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
module Settings
|
||||
module MediaWikiSettings
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
field :mediawiki_public_url, type: :string,
|
||||
default: ENV["MEDIAWIKI_PUBLIC_URL"].presence
|
||||
|
||||
field :mediawiki_enabled, type: :boolean,
|
||||
default: ENV["MEDIAWIKI_PUBLIC_URL"].present?
|
||||
end
|
||||
end
|
||||
end
|
||||
38
app/models/concerns/settings/nostr_settings.rb
Normal file
38
app/models/concerns/settings/nostr_settings.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
module Settings
|
||||
module NostrSettings
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
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_public_key_primary_domain, type: :string,
|
||||
default: ENV["NOSTR_PUBLIC_KEY_PRIMARY_DOMAIN"].presence
|
||||
|
||||
field :nostr_relay_url, type: :string,
|
||||
default: ENV["NOSTR_RELAY_URL"].presence
|
||||
|
||||
field :nostr_zaps_relay_limit, type: :integer,
|
||||
default: 12
|
||||
|
||||
field :nostr_discovery_relays, type: :array, default: %w[
|
||||
wss://nostr.kosmos.org
|
||||
wss://purplepag.es
|
||||
wss://relay.nostr.band
|
||||
wss://njump.me
|
||||
wss://relay.damus.io
|
||||
]
|
||||
|
||||
def self.nostr_relay_url_http
|
||||
self.nostr_relay_url.gsub(/^ws:/, "http:")
|
||||
.gsub(/^wss:/, "https:")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
9
app/models/concerns/settings/open_collective_settings.rb
Normal file
9
app/models/concerns/settings/open_collective_settings.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
module Settings
|
||||
module OpenCollectiveSettings
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
field :opencollective_enabled, type: :boolean, default: true
|
||||
end
|
||||
end
|
||||
end
|
||||
16
app/models/concerns/settings/remote_storage_settings.rb
Normal file
16
app/models/concerns/settings/remote_storage_settings.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
module Settings
|
||||
module RemoteStorageSettings
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
field :remotestorage_enabled, type: :boolean,
|
||||
default: ENV["RS_STORAGE_URL"].present?
|
||||
|
||||
field :rs_storage_url, type: :string,
|
||||
default: ENV["RS_STORAGE_URL"].presence
|
||||
|
||||
field :rs_redis_url, type: :string,
|
||||
default: ENV["RS_REDIS_URL"] || "redis://localhost:6379/1"
|
||||
end
|
||||
end
|
||||
end
|
||||
11
app/models/concerns/settings/xmpp_settings.rb
Normal file
11
app/models/concerns/settings/xmpp_settings.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
module Settings
|
||||
module XmppSettings
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
field :xmpp_default_rooms, type: :array, default: []
|
||||
field :xmpp_autojoin_default_rooms, type: :boolean, default: false
|
||||
field :xmpp_notifications_from_address, type: :string, default: primary_domain
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,223 +2,30 @@
|
||||
class Setting < RailsSettings::Base
|
||||
cache_prefix { "v1" }
|
||||
|
||||
field :primary_domain, type: :string,
|
||||
default: ENV["PRIMARY_DOMAIN"].presence
|
||||
|
||||
field :accounts_domain, type: :string,
|
||||
default: ENV["AKKOUNTS_DOMAIN"].presence
|
||||
|
||||
#
|
||||
# Internal services
|
||||
#
|
||||
|
||||
field :redis_url, type: :string,
|
||||
default: ENV["REDIS_URL"] || "redis://localhost:6379/0"
|
||||
|
||||
field :s3_enabled, type: :boolean,
|
||||
default: ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false"
|
||||
|
||||
#
|
||||
# Registrations
|
||||
#
|
||||
|
||||
field :reserved_usernames, type: :array, default: %w[
|
||||
account accounts donations mail webmaster support
|
||||
]
|
||||
|
||||
#
|
||||
# XMPP
|
||||
#
|
||||
|
||||
field :xmpp_default_rooms, type: :array, default: []
|
||||
field :xmpp_autojoin_default_rooms, type: :boolean, default: false
|
||||
field :xmpp_notifications_from_address, type: :string, default: primary_domain
|
||||
|
||||
#
|
||||
# Sentry
|
||||
#
|
||||
|
||||
field :sentry_enabled, type: :boolean, readonly: true,
|
||||
default: ENV["SENTRY_DSN"].present?
|
||||
|
||||
#
|
||||
# BTCPay Server
|
||||
#
|
||||
|
||||
field :btcpay_api_url, type: :string,
|
||||
default: ENV["BTCPAY_API_URL"].presence
|
||||
|
||||
field :btcpay_enabled, type: :boolean,
|
||||
default: ENV["BTCPAY_API_URL"].present?
|
||||
|
||||
field :btcpay_public_url, type: :string,
|
||||
default: ENV["BTCPAY_PUBLIC_URL"].presence
|
||||
|
||||
field :btcpay_store_id, type: :string,
|
||||
default: ENV["BTCPAY_STORE_ID"].presence
|
||||
|
||||
field :btcpay_auth_token, type: :string,
|
||||
default: ENV["BTCPAY_AUTH_TOKEN"].presence
|
||||
|
||||
field :btcpay_publish_wallet_balances, type: :boolean, default: true
|
||||
|
||||
#
|
||||
# Discourse
|
||||
#
|
||||
|
||||
field :discourse_public_url, type: :string,
|
||||
default: ENV["DISCOURSE_PUBLIC_URL"].presence
|
||||
|
||||
field :discourse_enabled, type: :boolean,
|
||||
default: ENV["DISCOURSE_PUBLIC_URL"].present?
|
||||
|
||||
field :discourse_connect_secret, type: :string,
|
||||
default: ENV["DISCOURSE_CONNECT_SECRET"].presence
|
||||
|
||||
#
|
||||
# Drone CI
|
||||
#
|
||||
|
||||
field :droneci_public_url, type: :string,
|
||||
default: ENV["DRONECI_PUBLIC_URL"].presence
|
||||
|
||||
field :droneci_enabled, type: :boolean,
|
||||
default: ENV["DRONECI_PUBLIC_URL"].present?
|
||||
|
||||
#
|
||||
# ejabberd
|
||||
#
|
||||
|
||||
field :ejabberd_enabled, type: :boolean,
|
||||
default: ENV["EJABBERD_API_URL"].present?
|
||||
|
||||
field :ejabberd_api_url, type: :string,
|
||||
default: ENV["EJABBERD_API_URL"].presence
|
||||
|
||||
field :ejabberd_admin_url, type: :string,
|
||||
default: ENV["EJABBERD_ADMIN_URL"].presence
|
||||
|
||||
field :ejabberd_buddy_roster, type: :string,
|
||||
default: "Buddies"
|
||||
|
||||
#
|
||||
# Gitea
|
||||
#
|
||||
|
||||
field :gitea_public_url, type: :string,
|
||||
default: ENV["GITEA_PUBLIC_URL"].presence
|
||||
|
||||
field :gitea_enabled, type: :boolean,
|
||||
default: ENV["GITEA_PUBLIC_URL"].present?
|
||||
|
||||
#
|
||||
# Lightning Network
|
||||
#
|
||||
|
||||
field :lndhub_api_url, type: :string,
|
||||
default: ENV["LNDHUB_API_URL"].presence
|
||||
|
||||
field :lndhub_enabled, type: :boolean,
|
||||
default: ENV["LNDHUB_API_URL"].present?
|
||||
|
||||
field :lndhub_admin_token, type: :string,
|
||||
default: ENV["LNDHUB_ADMIN_TOKEN"].presence
|
||||
|
||||
field :lndhub_admin_enabled, type: :boolean,
|
||||
default: ENV["LNDHUB_ADMIN_UI"] || false
|
||||
|
||||
field :lndhub_public_key, type: :string,
|
||||
default: (ENV["LNDHUB_PUBLIC_KEY"] || "")
|
||||
|
||||
field :lndhub_keysend_enabled, type: :boolean,
|
||||
default: -> { self.lndhub_public_key.present? }
|
||||
|
||||
#
|
||||
# Mastodon
|
||||
#
|
||||
|
||||
field :mastodon_public_url, type: :string,
|
||||
default: ENV["MASTODON_PUBLIC_URL"].presence
|
||||
|
||||
field :mastodon_enabled, type: :boolean,
|
||||
default: ENV["MASTODON_PUBLIC_URL"].present?
|
||||
|
||||
field :mastodon_address_domain, type: :string,
|
||||
default: ENV["MASTODON_ADDRESS_DOMAIN"].presence || self.primary_domain
|
||||
|
||||
#
|
||||
# MediaWiki
|
||||
#
|
||||
|
||||
field :mediawiki_public_url, type: :string,
|
||||
default: ENV["MEDIAWIKI_PUBLIC_URL"].presence
|
||||
|
||||
field :mediawiki_enabled, type: :boolean,
|
||||
default: ENV["MEDIAWIKI_PUBLIC_URL"].present?
|
||||
|
||||
#
|
||||
# Nostr
|
||||
#
|
||||
|
||||
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_zaps_relay_limit, type: :integer,
|
||||
default: 12
|
||||
|
||||
#
|
||||
# OpenCollective
|
||||
#
|
||||
|
||||
field :opencollective_enabled, type: :boolean, default: true
|
||||
|
||||
#
|
||||
# RemoteStorage
|
||||
#
|
||||
|
||||
field :remotestorage_enabled, type: :boolean,
|
||||
default: ENV["RS_STORAGE_URL"].present?
|
||||
|
||||
field :rs_storage_url, type: :string,
|
||||
default: ENV["RS_STORAGE_URL"].presence
|
||||
|
||||
field :rs_redis_url, type: :string,
|
||||
default: ENV["RS_REDIS_URL"] || "redis://localhost:6379/1"
|
||||
|
||||
|
||||
#
|
||||
# E-Mail Service
|
||||
#
|
||||
|
||||
field :email_enabled, type: :boolean,
|
||||
default: ENV["EMAIL_SMTP_HOST"].present?
|
||||
|
||||
# field :email_smtp_host, type: :string,
|
||||
# default: ENV["EMAIL_SMTP_HOST"].presence
|
||||
#
|
||||
# field :email_smtp_port, type: :string,
|
||||
# default: ENV["EMAIL_SMTP_PORT"].presence || 587
|
||||
#
|
||||
# field :email_smtp_enable_starttls, type: :string,
|
||||
# default: ENV["EMAIL_SMTP_PORT"].presence || true
|
||||
#
|
||||
# field :email_auth_method, type: :string,
|
||||
# default: ENV["EMAIL_AUTH_METHOD"].presence || "plain"
|
||||
#
|
||||
# field :email_imap_host, type: :string,
|
||||
# default: ENV["EMAIL_IMAP_HOST"].presence
|
||||
#
|
||||
# field :email_imap_port, type: :string,
|
||||
# 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 ]
|
||||
Dir[Rails.root.join('app', 'models', 'concerns', 'settings', '*.rb')].each do |file|
|
||||
require file
|
||||
end
|
||||
|
||||
include Settings::GeneralSettings
|
||||
include Settings::BtcpaySettings
|
||||
include Settings::DiscourseSettings
|
||||
include Settings::DroneCiSettings
|
||||
include Settings::EjabberdSettings
|
||||
include Settings::EmailSettings
|
||||
include Settings::GiteaSettings
|
||||
include Settings::LightningNetworkSettings
|
||||
include Settings::MastodonSettings
|
||||
include Settings::MediaWikiSettings
|
||||
include Settings::NostrSettings
|
||||
include Settings::OpenCollectiveSettings
|
||||
include Settings::RemoteStorageSettings
|
||||
include Settings::XmppSettings
|
||||
|
||||
def self.available_services
|
||||
known_services = SERVICES[:external].keys
|
||||
known_services.select {|s| Setting.send "#{s}_enabled?" }
|
||||
end
|
||||
|
||||
field :default_services, type: :array,
|
||||
default: self.available_services
|
||||
end
|
||||
|
||||
@@ -180,14 +180,14 @@ class User < ApplicationRecord
|
||||
def enable_service(service)
|
||||
current_services = services_enabled
|
||||
new_services = Array(service).map(&:to_s)
|
||||
services = (current_services + new_services).uniq
|
||||
services = (current_services + new_services).uniq.sort
|
||||
ldap.replace_attribute(dn, :serviceEnabled, services)
|
||||
end
|
||||
|
||||
def disable_service(service)
|
||||
current_services = services_enabled
|
||||
disabled_services = Array(service).map(&:to_s)
|
||||
services = (current_services - disabled_services).uniq
|
||||
services = (current_services - disabled_services).uniq.sort
|
||||
ldap.replace_attribute(dn, :serviceEnabled, services)
|
||||
end
|
||||
|
||||
|
||||
@@ -26,4 +26,8 @@ class UserPreferences
|
||||
end
|
||||
hash.stringify_keys!.to_h
|
||||
end
|
||||
|
||||
def self.pref_keys
|
||||
DEFAULT_PREFS.keys.map(&:to_sym)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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
|
||||
|
||||
@@ -101,7 +101,7 @@ class LdapService < ApplicationService
|
||||
dn = "ou=#{ou},cn=users,#{ldap_suffix}"
|
||||
|
||||
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
|
||||
|
||||
attrs = {
|
||||
|
||||
21
app/services/nostr_manager/discover_user_profile.rb
Normal file
21
app/services/nostr_manager/discover_user_profile.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
module NostrManager
|
||||
class DiscoverUserProfile < NostrManagerService
|
||||
def initialize(pubkey:, relays: nil)
|
||||
@pubkey = pubkey
|
||||
@relays = relays.present? ? relays : Setting.nostr_discovery_relays
|
||||
end
|
||||
|
||||
def call
|
||||
filter = Nostr::Filter.new(
|
||||
authors: [@pubkey],
|
||||
kinds: [0],
|
||||
limit: 1,
|
||||
)
|
||||
|
||||
NostrManager::FetchLatestEvent.call(
|
||||
relays: @relays,
|
||||
filter: filter
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
21
app/services/nostr_manager/discover_user_relays.rb
Normal file
21
app/services/nostr_manager/discover_user_relays.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
module NostrManager
|
||||
class DiscoverUserRelays < NostrManagerService
|
||||
def initialize(pubkey:)
|
||||
@pubkey = pubkey
|
||||
@relays = Setting.nostr_discovery_relays
|
||||
end
|
||||
|
||||
def call
|
||||
filter = Nostr::Filter.new(
|
||||
authors: [@pubkey],
|
||||
kinds: [10002],
|
||||
limit: 1,
|
||||
)
|
||||
|
||||
NostrManager::FetchLatestEvent.call(
|
||||
relays: @relays,
|
||||
filter: filter
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
59
app/services/nostr_manager/fetch_event.rb
Normal file
59
app/services/nostr_manager/fetch_event.rb
Normal file
@@ -0,0 +1,59 @@
|
||||
module NostrManager
|
||||
class FetchEvent < NostrManagerService
|
||||
TIMEOUT = 10
|
||||
|
||||
def initialize(filter:, relay_url:)
|
||||
@filter = filter
|
||||
@relay = new_relay(relay_url)
|
||||
@client = Nostr::Client.new
|
||||
end
|
||||
|
||||
def call
|
||||
filter, client, relay = @filter, @client, @relay
|
||||
event = nil
|
||||
mutex = Mutex.new
|
||||
received_event = ConditionVariable.new
|
||||
log_prefix = "[nostr][#{@relay.name}]"
|
||||
|
||||
thread = Thread.new do
|
||||
client.on :connect do
|
||||
client.subscribe(filter: filter)
|
||||
end
|
||||
|
||||
client.on :error do |e|
|
||||
Rails.logger.info "#{log_prefix} Error: #{e}"
|
||||
Thread.current.exit
|
||||
end
|
||||
|
||||
client.on :message do |m|
|
||||
msg = JSON.parse(m) rescue nil
|
||||
if msg && msg[0] == "EVENT" && msg[2]
|
||||
Rails.logger.debug "#{log_prefix} Event received: #{msg[2]["id"]}"
|
||||
mutex.synchronize do
|
||||
event = msg[2]
|
||||
received_event.signal
|
||||
end
|
||||
elsif msg && msg[0] == "EOSE"
|
||||
Thread.current.exit
|
||||
end
|
||||
end
|
||||
|
||||
client.connect relay
|
||||
end
|
||||
|
||||
begin
|
||||
Timeout.timeout(TIMEOUT) do
|
||||
mutex.synchronize do
|
||||
received_event.wait(mutex) if event.nil?
|
||||
end
|
||||
end
|
||||
rescue Timeout::Error
|
||||
Rails.logger.debug "#{log_prefix} Timeout: No event received within #{TIMEOUT} seconds"
|
||||
ensure
|
||||
thread.exit
|
||||
end
|
||||
|
||||
event
|
||||
end
|
||||
end
|
||||
end
|
||||
44
app/services/nostr_manager/fetch_latest_event.rb
Normal file
44
app/services/nostr_manager/fetch_latest_event.rb
Normal file
@@ -0,0 +1,44 @@
|
||||
module NostrManager
|
||||
class FetchLatestEvent < NostrManagerService
|
||||
TIMEOUT = 20
|
||||
|
||||
def initialize(relays:, filter:, max_events: 2)
|
||||
@relays = relays
|
||||
@filter = filter
|
||||
@max_events = max_events
|
||||
end
|
||||
|
||||
def call
|
||||
received_events = 0
|
||||
events = []
|
||||
|
||||
begin
|
||||
Timeout.timeout(TIMEOUT) do
|
||||
@relays.each do |url|
|
||||
event = NostrManager::FetchEvent.call(filter: @filter, relay_url: url)
|
||||
|
||||
if event.present?
|
||||
events << event if events.none? { |e| e["id"] == event["id"] }
|
||||
received_events += 1
|
||||
end
|
||||
|
||||
if received_events >= @max_events
|
||||
Rails.logger.debug "Found #{@max_events} events, ending the search"
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
events.min_by { |e| e["created_at"] }
|
||||
end
|
||||
rescue Timeout::Error
|
||||
if events.size == 1
|
||||
Rails.logger.debug "[nostr] Timeout: only found 1 event within #{TIMEOUT} seconds for filter: #{@filter.inspect}"
|
||||
events.first
|
||||
else
|
||||
Rails.logger.debug "[nostr] Timeout: no events found within #{TIMEOUT} seconds for filter: #{@filter.inspect}"
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -19,28 +19,28 @@ module NostrManager
|
||||
|
||||
thread = Thread.new do
|
||||
client.on :connect do
|
||||
puts "#{log_prefix} Publishing #{event.id}..."
|
||||
Rails.logger.debug "#{log_prefix} Publishing #{event.id}..."
|
||||
client.publish event
|
||||
end
|
||||
|
||||
client.on :error do |e|
|
||||
puts "#{log_prefix} Error: #{e}"
|
||||
puts "#{log_prefix} Closing thread..."
|
||||
Rails.logger.debug "#{log_prefix} Error: #{e}"
|
||||
Rails.logger.debug "#{log_prefix} Closing thread..."
|
||||
thread.exit
|
||||
end
|
||||
|
||||
client.on :message do |m|
|
||||
puts "#{log_prefix} Message: #{m}"
|
||||
Rails.logger.debug "#{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..."
|
||||
Rails.logger.debug "#{log_prefix} Event published. Closing thread..."
|
||||
else
|
||||
puts "#{log_prefix} Unexpected message from relay. Closing thread..."
|
||||
Rails.logger.debug "#{log_prefix} Unexpected message from relay. Closing thread..."
|
||||
end
|
||||
thread.exit
|
||||
end
|
||||
|
||||
puts "#{log_prefix} Connecting to #{relay.url}..."
|
||||
Rails.logger.debug "#{log_prefix} Connecting to #{relay.url}..."
|
||||
client.connect relay
|
||||
end
|
||||
|
||||
|
||||
@@ -6,8 +6,13 @@ module NostrManager
|
||||
|
||||
def call
|
||||
tags = parse_tags(@zap.request_event.tags)
|
||||
relays = tags[:relays].take(Setting.nostr_zaps_relay_limit)
|
||||
|
||||
tags[:relays].take(Setting.nostr_zaps_relay_limit).each do |relay_url|
|
||||
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
|
||||
|
||||
@@ -3,6 +3,7 @@ require "nostr"
|
||||
class NostrManagerService < ApplicationService
|
||||
def parse_tags(tags)
|
||||
out = {}
|
||||
# TODO support more than 1 item for each tag type
|
||||
tags.each do |tag|
|
||||
out[tag[0].to_sym] = tag[1, tag.length]
|
||||
end
|
||||
@@ -19,4 +20,8 @@ class NostrManagerService < ApplicationService
|
||||
def site_user
|
||||
Nostr::User.new(keypair: site_keypair)
|
||||
end
|
||||
|
||||
def new_relay(url)
|
||||
Nostr::Relay.new(url: url, name: URI.parse(url).host)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -38,8 +38,8 @@
|
||||
<tr>
|
||||
<td class="overflow-ellipsis font-mono"><%= invitation.token %></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.invitee.address, admin_user_path(invitation.invitee.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.cn, admin_user_path(invitation.invitee.cn), class: "ks-text-link" %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<% 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 %>
|
||||
</td>
|
||||
<td><%= number_with_delimiter account.balance.to_i.to_s %></td>
|
||||
|
||||
@@ -9,18 +9,36 @@
|
||||
<%= render partial: "admin/settings/errors", locals: { errors: @errors } %>
|
||||
<% end %>
|
||||
|
||||
<label class="block">
|
||||
<p class="font-bold mb-1">Reserved usernames</p>
|
||||
<p class="text-gray-500">
|
||||
These usernames cannot be registered as accounts:
|
||||
</p>
|
||||
<%= f.text_area :reserved_usernames,
|
||||
value: Setting.reserved_usernames.join("\n"),
|
||||
class: "h-44 mb-2" %>
|
||||
<p class="text-sm text-gray-500">
|
||||
One username per line
|
||||
</p>
|
||||
</label>
|
||||
<ul role="list">
|
||||
<%= render FormElements::FieldsetComponent.new(
|
||||
title: "Reserved usernames",
|
||||
description: "These usernames cannot be registered as accounts."
|
||||
) do %>
|
||||
<%= f.text_area :reserved_usernames,
|
||||
value: Setting.reserved_usernames.join("\n"),
|
||||
class: "h-44 w-60" %>
|
||||
<p class="text-sm text-gray-500">
|
||||
One username per line
|
||||
</p>
|
||||
<% end %>
|
||||
<li>
|
||||
<p class="font-bold mb-1">Default services</p>
|
||||
<p class="text-gray-500">
|
||||
These services are enabled for new users by default after signup.
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-x-6 gap-y-2">
|
||||
<% Setting.available_services.each do |option| %>
|
||||
<div class="md:inline-block">
|
||||
<%= f.check_box :default_services,
|
||||
{ multiple: true, checked: Setting.default_services.include?(option),
|
||||
class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-600 mr-0.5" },
|
||||
option, nil %>
|
||||
<%= f.label "default_services_#{option.parameterize}", service_human_name(option) %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
|
||||
@@ -19,15 +19,40 @@
|
||||
title: "Public key",
|
||||
description: "The corresponding public key of the accounts service"
|
||||
) %>
|
||||
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||
key: :nostr_public_key_primary_domain,
|
||||
title: "Public key for primary domain (NIP-05)",
|
||||
description: "(optional) A different pubkey to announce for the _@#{Setting.primary_domain} Nostr address"
|
||||
) %>
|
||||
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||
key: :nostr_relay_url,
|
||||
title: "Relay URL",
|
||||
description: "Websockets URL of a relay associated with #{Setting.primary_domain}"
|
||||
) %>
|
||||
</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"
|
||||
description: "The maximum number of sender-defined relays to try to publish zap receipts to"
|
||||
) %>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>Onboarding</h3>
|
||||
<ul role="list">
|
||||
<%= render FormElements::FieldsetComponent.new(
|
||||
title: "Discovery relays",
|
||||
description: "Used to discover a user's published relay list and/or profile"
|
||||
) do %>
|
||||
<%= f.text_area :nostr_discovery_relays,
|
||||
value: Setting.nostr_discovery_relays.join("\n"),
|
||||
class: "h-44 w-80" %>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<h3>RemoteStorage</h3>
|
||||
<p class="text-red-600 mb-8">Feature currently in development.</p>
|
||||
<ul role="list">
|
||||
<%= render FormElements::FieldsetToggleComponent.new(
|
||||
form: f,
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<th>Invited by</th>
|
||||
<td>
|
||||
<% 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 %>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -78,7 +78,7 @@
|
||||
<% if @user.invitees.length > 0 %>
|
||||
<ul class="mb-0">
|
||||
<% @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 %>
|
||||
</ul>
|
||||
<% else %>—<% end %>
|
||||
@@ -124,6 +124,19 @@
|
||||
</td>
|
||||
</tr>
|
||||
<% 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 %>
|
||||
<tr>
|
||||
<td>Gitea</td>
|
||||
@@ -171,7 +184,7 @@
|
||||
<td>XMPP (ejabberd)</td>
|
||||
<td>
|
||||
<%= render FormElements::ToggleComponent.new(
|
||||
enabled: @services_enabled.include?("xmpp"),
|
||||
enabled: @services_enabled.include?("ejabberd"),
|
||||
input_enabled: false
|
||||
) %>
|
||||
</td>
|
||||
@@ -182,6 +195,33 @@
|
||||
</td>
|
||||
</tr>
|
||||
<% 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>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
</p>
|
||||
<p>
|
||||
We have run two 6-month trials so far, with the next trial period
|
||||
starting sometime in Q2 2024. Watch your email for notifications about it!
|
||||
starting sometime soon. Watch your email for notifications about it!
|
||||
</p>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="services grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
|
||||
<% if Setting.ejabberd_enabled? %>
|
||||
<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)]">
|
||||
<%= link_to services_chat_path,
|
||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||
@@ -18,7 +18,7 @@
|
||||
<% end %>
|
||||
<% if Setting.mastodon_enabled? %>
|
||||
<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)]">
|
||||
<%= link_to services_mastodon_path, class: "block h-full px-6 py-6 rounded-md" do %>
|
||||
<h3 class="mb-3.5">Mastodon</h3>
|
||||
@@ -30,7 +30,9 @@
|
||||
<% end %>
|
||||
<% if Setting.email_enabled? &&
|
||||
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 %>
|
||||
<h3 class="mb-3.5">E-Mail</h3>
|
||||
<p class="text-gray-600">
|
||||
@@ -39,15 +41,16 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if Setting.discourse_enabled? %>
|
||||
<% if Setting.remotestorage_enabled? &&
|
||||
Flipper.enabled?(:remotestorage, current_user) %>
|
||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||
bg-[length:95%] bg-center bg-no-repeat
|
||||
bg-[url(/img/logos/icon_discourse.svg)]">
|
||||
<%= link_to "#{Setting.discourse_public_url}/session/sso?return_path=/",
|
||||
bg-[length:80%] bg-[center_top_-156px] bg-no-repeat
|
||||
bg-[url(/img/logos/icon_remotestorage.svg)]">
|
||||
<%= link_to services_storage_path,
|
||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||
<h3 class="mb-3.5">Discourse</h3>
|
||||
<h3 class="mb-3.5">Storage</h3>
|
||||
<p class="text-gray-600">
|
||||
Kosmos community forums and user support/help site
|
||||
Sync your data between apps and devices
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -65,21 +68,22 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if Setting.remotestorage_enabled? &&
|
||||
Flipper.enabled?(:remotestorage, current_user) %>
|
||||
<div class="border border-gray-300 rounded-md hover:border-gray-400">
|
||||
<%= link_to services_storage_path,
|
||||
<% if Setting.discourse_enabled? %>
|
||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||
bg-[length:80%] bg-center bg-no-repeat
|
||||
bg-[url(/img/logos/icon_discourse.svg)]">
|
||||
<%= link_to "#{Setting.discourse_public_url}/session/sso?return_path=/",
|
||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||
<h3 class="mb-3.5">Storage</h3>
|
||||
<h3 class="mb-3.5">Discourse</h3>
|
||||
<p class="text-gray-600">
|
||||
Sync your data between apps and devices
|
||||
Community forums and support/help site
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if Setting.gitea_enabled? %>
|
||||
<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)]">
|
||||
<%= link_to Setting.gitea_public_url,
|
||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||
@@ -92,7 +96,7 @@
|
||||
<% end %>
|
||||
<% if Setting.droneci_enabled? %>
|
||||
<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)]">
|
||||
<%= link_to Setting.droneci_public_url,
|
||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||
|
||||
@@ -100,6 +100,14 @@
|
||||
["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 id="apps-windows" class="hidden grid grid-cols-1 gap-6"
|
||||
data-tabs-target="panel">
|
||||
|
||||
@@ -2,15 +2,162 @@
|
||||
|
||||
<%= render MainSimpleComponent.new do %>
|
||||
<section>
|
||||
<h3 class="mb-10">Connected Apps</h3>
|
||||
<% if @rs_auths.any? %>
|
||||
<div class="w-full grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-y-10 gap-x-12">
|
||||
<% @rs_auths.each do |auth| %>
|
||||
<%= render RsAuthComponent.new(auth: auth) %>
|
||||
<% end %>
|
||||
<p class="mb-6">
|
||||
Store and synchronize your app data across different devices.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<%= render partial: "shared/tabnav_remotestorage" %>
|
||||
|
||||
<section>
|
||||
<h3>Your Storage Address</h3>
|
||||
<p class="mb-6">
|
||||
In order to connect an app to your storage account, give it your address:
|
||||
</p>
|
||||
<p data-controller="clipboard" class="flex items-center gap-1 sm:w-2/5">
|
||||
<img src="/img/logos/icon_remotestorage.svg" class="inline-block h-6 w-6 mr-1">
|
||||
<input type="text" id="user_address" class="grow"
|
||||
value=<%= current_user.address %> disabled="disabled"
|
||||
data-clipboard-target="source" />
|
||||
<button id="copy-user-address" class="btn-md btn-icon btn-outline shrink-0"
|
||||
data-clipboard-target="trigger" data-action="clipboard#copy"
|
||||
title="Copy to clipboard">
|
||||
<span class="content-initial">
|
||||
<%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||
</span>
|
||||
<span class="content-active hidden">
|
||||
<%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||
</span>
|
||||
</button>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>Compatible Apps</h3>
|
||||
<p>
|
||||
Your Storage account is based on a new open standard called
|
||||
<a href="https://remotestorage.io" target="_blank">
|
||||
<img src="/img/logos/icon_remotestorage.svg" class="h-4 w-4 inline">
|
||||
<strong>remoteStorage</strong>
|
||||
</a>, which is not yet widely supported. Look
|
||||
for the remoteStorage icon, or check the Sync settings in apps.
|
||||
</p>
|
||||
<p>
|
||||
If you want your favorite apps to support syncing data with your own
|
||||
Storage account, let the developers know! All relevant information is
|
||||
available on the <a href="https://remotestorage.io"
|
||||
target="_blank" class="ks-text-link">remoteStorage website</a>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>Recommended Apps</h3>
|
||||
<div data-controller="tabs"
|
||||
data-tabs-active-tab-class="-mb-px border-gray-200 border-l border-t border-r rounded-t text-indigo-600 hover:text-indigo-600"
|
||||
data-tabs-inactive-tab-class="text-gray-500 hover:text-gray-700"
|
||||
class="mb-12">
|
||||
<select data-action="tabs#change" data-tabs-target="select"
|
||||
class="block w-full mb-8 sm:hidden">
|
||||
<option>Productivity</option>
|
||||
<option>Bookmarks</option>
|
||||
<option>Reading</option>
|
||||
<option>File sharing</option>
|
||||
<option>Learning</option>
|
||||
</select>
|
||||
<ul class="hidden sm:flex list-reset mb-8 border-gray-200 border-b">
|
||||
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
|
||||
Productivity
|
||||
</a>
|
||||
</li>
|
||||
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
|
||||
Bookmarks
|
||||
</a>
|
||||
</li>
|
||||
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
|
||||
Reading
|
||||
</a>
|
||||
</li>
|
||||
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
|
||||
File sharing
|
||||
</a>
|
||||
</li>
|
||||
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
|
||||
Learning
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Hyperdraft",
|
||||
description: "Create text notes and (optionally) turn them into a website",
|
||||
icon_path: "/img/app_icons/hyperdraft.png",
|
||||
links: [
|
||||
["Website", "https://hyperdraft.rosano.ca"],
|
||||
]
|
||||
) %>
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Notes Together",
|
||||
description: "A powerful note-taking app, with support for attaching images and other files",
|
||||
icon_path: "/img/app_icons/notes-together.png",
|
||||
links: [
|
||||
["Web App", "https://notestogether.hominidsoftware.com"],
|
||||
]
|
||||
) %>
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Papiers",
|
||||
description: "A simple note-taking app",
|
||||
icon_path: "/img/app_icons/papiers.png",
|
||||
links: [
|
||||
["Web App", "https://papiers.gitlab.io"],
|
||||
]
|
||||
) %>
|
||||
</div>
|
||||
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Webmarks",
|
||||
description: "Archive your bookmarks in your remote storage",
|
||||
icon_path: "/img/app_icons/webmarks.png",
|
||||
links: [
|
||||
["Web App", "https://webmarks.5apps.com"],
|
||||
]
|
||||
) %>
|
||||
</div>
|
||||
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Pétrolette",
|
||||
description: "A news aggregator that syncs with your remote storage",
|
||||
icon_path: "/img/app_icons/petrolette.png",
|
||||
links: [
|
||||
["Web App", "https://petrolette.space"],
|
||||
]
|
||||
) %>
|
||||
</div>
|
||||
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Sharesome",
|
||||
description: "Quickly and easily share files from your remote storage",
|
||||
icon_path: "/img/app_icons/sharesome.png",
|
||||
links: [
|
||||
["Web App", "https://sharesome.5apps.com"],
|
||||
]
|
||||
) %>
|
||||
</div>
|
||||
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Kommit",
|
||||
description: "Create flashcards and learn them with spaced-repetition",
|
||||
icon_path: "/img/app_icons/kommit.png",
|
||||
links: [
|
||||
["Website", "https://kommit.rosano.ca"],
|
||||
]
|
||||
) %>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<p>No apps connected yet.</p>
|
||||
<% end %>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
33
app/views/services/rs_auths/index.html.erb
Normal file
33
app/views/services/rs_auths/index.html.erb
Normal file
@@ -0,0 +1,33 @@
|
||||
<%= render HeaderComponent.new(title: "Storage") %>
|
||||
|
||||
<%= render MainSimpleComponent.new do %>
|
||||
<section>
|
||||
<p class="mb-6">
|
||||
Store and synchronize your app data across different devices.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<%= render partial: "shared/tabnav_remotestorage" %>
|
||||
|
||||
<section>
|
||||
<% if @rs_auths.any? %>
|
||||
<div class="w-full grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-y-10 gap-x-12 mt-4">
|
||||
<% @rs_auths.each do |auth| %>
|
||||
<%= render RsAuthComponent.new(auth: auth) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-center">
|
||||
<p class="mt-4 mb-12 inline-flex align-center items-center">
|
||||
<%= image_tag("/img/illustrations/undraw_friends_r511.svg", class: 'h-48') %>
|
||||
</p>
|
||||
<h3>
|
||||
No apps connected
|
||||
</h3>
|
||||
<p class="text-gray-500">
|
||||
When connected, your apps will show up here.
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</section>
|
||||
<% end %>
|
||||
@@ -5,7 +5,7 @@
|
||||
<%= render FormElements::FieldsetComponent.new(
|
||||
positioning: :horizontal,
|
||||
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 %>
|
||||
<% f.fields_for :preferences do |p| %>
|
||||
<%= p.select :lightning_notify_sats_received, options_for_select([
|
||||
@@ -15,6 +15,38 @@
|
||||
], selected: @user.preferences[:lightning_notify_sats_received]) %>
|
||||
<% 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>
|
||||
</section>
|
||||
<section>
|
||||
|
||||
@@ -1,47 +1,43 @@
|
||||
<section>
|
||||
<h3>Nostr</h3>
|
||||
<h4 class="mb-0">Public Key</h4>
|
||||
<div data-controller="settings--nostr-pubkey"
|
||||
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-pubkey-hex-value="<%= current_user.nostr_pubkey %>">
|
||||
|
||||
<p class="<%= current_user.nostr_pubkey.present? ? '' : 'hidden' %> mt-2 flex gap-1">
|
||||
<div data-controller="settings--nostr-pubkey"
|
||||
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-pubkey-hex-value="<%= current_user.nostr_pubkey %>">
|
||||
<section class="mb-8 sm:mb-12">
|
||||
<h3>Nostr</h3>
|
||||
<h4 class="mb-0">
|
||||
Public Key
|
||||
</h4>
|
||||
<p class="<%= current_user.nostr_pubkey.present? ? '' : 'hidden' %> mt-2 flex gap-x-1">
|
||||
<input type="text" value="<%= current_user.nostr_pubkey_bech32 %>" disabled
|
||||
data-settings--nostr-pubkey-target="pubkeyBech32Input"
|
||||
name="nostr_public_key" class="relative grow" />
|
||||
name="nostr_public_key" class="w-full" />
|
||||
<%= link_to nostr_pubkey_settings_path,
|
||||
class: 'btn-md btn-outline text-red-700 relative shrink-0',
|
||||
class: 'btn-md btn-outline relative grow-0 shrink-0 text-red-700',
|
||||
data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } do %>
|
||||
Remove
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<% if current_user.nostr_pubkey.present? %>
|
||||
<div class="rounded-md bg-blue-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<p class="text-sm text-blue-800">
|
||||
Your user address <strong><%= current_user.address %></strong> is
|
||||
also a Nostr address now. Use your favorite Nostr app, or for
|
||||
example <a href="http://metadata.nostr.com" target="_blank"
|
||||
class="underline">metadata.nostr.com</a>, to add this
|
||||
<strong>NIP-05</strong> address to your public profile.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div> -->
|
||||
<!-- Pubkey present -->
|
||||
<!-- </div> -->
|
||||
<% else %>
|
||||
<p class="my-4">
|
||||
If you use any apps on the Nostr network, you can verify your public key
|
||||
with us in order to enable Nostr-specific features for your account.
|
||||
Verify your Nostr public key with us in order to enable Nostr-specific
|
||||
features for your account:
|
||||
</p>
|
||||
<ul class="list-disc list-inside">
|
||||
<li>Log in with Nostr (no password needed)</li>
|
||||
<li>Verified Nostr address</li>
|
||||
<% if Setting.lndhub_enabled? %>
|
||||
<li>Receive zaps in your Lightning account</li>
|
||||
<% end %>
|
||||
<% if Setting.nostr_relay_url.present? %>
|
||||
<li>Publish notes on <%= link_to "our relay", Setting.nostr_relay_url_http, class: "ks-text-link", target: "_blank" %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
|
||||
<div data-settings--nostr-pubkey-target="noExtension"
|
||||
@@ -58,8 +54,8 @@
|
||||
</h3>
|
||||
<div class="mt-2 mb-0 text-sm text-blue-800">
|
||||
<p>
|
||||
We recommend Alby, which you can also use for your Lightning
|
||||
Wallet.
|
||||
We recommend Alby, which you can also use a wallet for your
|
||||
Lightning account.
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
@@ -86,5 +82,11 @@
|
||||
</button>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<% if current_user.nostr_pubkey.present? %>
|
||||
<%= turbo_frame_tag "nostr_user_metadata", src: nostr_user_metadata_settings_path do %>
|
||||
<p>Loading...</p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
27
app/views/settings/_nostr_user_metadata.html.erb
Normal file
27
app/views/settings/_nostr_user_metadata.html.erb
Normal file
@@ -0,0 +1,27 @@
|
||||
<%= turbo_frame_tag "nostr_user_metadata" do %>
|
||||
<section>
|
||||
<h3>Relays</h3>
|
||||
<%= render Settings::NostrRelayStatusComponent.new(
|
||||
nip65_event: @nip65_event
|
||||
) %>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>Profile</h3>
|
||||
<%= render Settings::NostrProfileStatusComponent.new(
|
||||
profile_event: @profile,
|
||||
user_address: current_user.address
|
||||
) %>
|
||||
<div class="mt-8" data-controller="modal" data-action="keydown.esc->modal#close">
|
||||
<button data-action="click->modal#open" class="btn-md btn-blue w-full sm:w-auto">
|
||||
Edit profile
|
||||
</button>
|
||||
<%= render ModalComponent.new(show_close_button: false) do %>
|
||||
<%= render Settings::NostrEditProfileComponent.new(
|
||||
user: current_user,
|
||||
profile_event: @profile
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
</section>
|
||||
<% end %>
|
||||
@@ -19,12 +19,6 @@
|
||||
active: @settings_section.to_s == "email"
|
||||
) %>
|
||||
<% end %>
|
||||
<% if Setting.lndhub_enabled %>
|
||||
<%= render SidenavLinkComponent.new(
|
||||
name: "Lightning", path: setting_path(:lightning), icon: "zap",
|
||||
active: @settings_section.to_s == "lightning"
|
||||
) %>
|
||||
<% end %>
|
||||
<% if Setting.remotestorage_enabled? &&
|
||||
Flipper.enabled?(:remotestorage, current_user) %>
|
||||
<%= render SidenavLinkComponent.new(
|
||||
@@ -32,6 +26,12 @@
|
||||
active: @settings_section.to_s == "remotestorage"
|
||||
) %>
|
||||
<% end %>
|
||||
<% if Setting.lndhub_enabled %>
|
||||
<%= render SidenavLinkComponent.new(
|
||||
name: "Lightning", path: setting_path(:lightning), icon: "zap",
|
||||
active: @settings_section.to_s == "lightning"
|
||||
) %>
|
||||
<% end %>
|
||||
<% if Setting.nostr_enabled %>
|
||||
<%= render SidenavLinkComponent.new(
|
||||
name: "Nostr", path: setting_path(:nostr), icon: "nostrich-head",
|
||||
|
||||
14
app/views/shared/_tabnav_remotestorage.html.erb
Normal file
14
app/views/shared/_tabnav_remotestorage.html.erb
Normal file
@@ -0,0 +1,14 @@
|
||||
<section>
|
||||
<div class="border-b border-gray-200">
|
||||
<nav class="-mb-px flex" aria-label="Tabs">
|
||||
<%= render TabnavLinkComponent.new(
|
||||
name: "Info", path: services_storage_path,
|
||||
active: current_page?(services_storage_path)
|
||||
) %>
|
||||
<%= render TabnavLinkComponent.new(
|
||||
name: "Connected Apps", path: apps_services_storage_path,
|
||||
active: current_page?(apps_services_storage_path)
|
||||
) %>
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
3
app/views/shared/nostr/_edit_user_profile.html.erb
Normal file
3
app/views/shared/nostr/_edit_user_profile.html.erb
Normal file
@@ -0,0 +1,3 @@
|
||||
<div>
|
||||
<%= profile.inspect %>
|
||||
</div>
|
||||
@@ -1,3 +1,6 @@
|
||||
lightning_notify_sats_received: disabled # or xmpp, email
|
||||
remotestorage_notify_auth_created: email # or xmpp, email
|
||||
lightning_notify_sats_received: 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
|
||||
|
||||
2
config/initializers/service_details.rb
Normal file
2
config/initializers/service_details.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
config_path = Rails.root.join('config', 'services.yml')
|
||||
SERVICES = YAML.load_file(config_path).deep_symbolize_keys.with_indifferent_access
|
||||
@@ -48,7 +48,8 @@ Rails.application.routes.draw do
|
||||
end
|
||||
|
||||
resource :storage, controller: 'remotestorage', only: [:show] do
|
||||
resources :rs_auths, only: [:destroy] do
|
||||
get :apps, to: "rs_auths#index"
|
||||
resources :rs_auths, only: [:index, :destroy] do
|
||||
member do
|
||||
get :revoke, to: 'rs_auths#destroy'
|
||||
get :launch_app
|
||||
@@ -64,6 +65,7 @@ Rails.application.routes.draw do
|
||||
post 'reset_email_password'
|
||||
post 'set_nostr_pubkey'
|
||||
delete 'nostr_pubkey', to: 'settings#remove_nostr_pubkey'
|
||||
get 'fetch_nostr_user_metadata', as: 'nostr_user_metadata'
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
30
config/services.yml
Normal file
30
config/services.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
internal:
|
||||
btcpay:
|
||||
name: BTCPay Server
|
||||
postgres:
|
||||
name: PostgreSQL
|
||||
sentry:
|
||||
name: Sentry
|
||||
external:
|
||||
discourse:
|
||||
name: Discourse
|
||||
droneci:
|
||||
name: Drone CI
|
||||
ejabberd:
|
||||
display_name: Chat
|
||||
email:
|
||||
name: E-Mail
|
||||
gitea:
|
||||
name: Gitea
|
||||
lndhub:
|
||||
name: LNDHub
|
||||
display_name: Lightning Network
|
||||
mastodon:
|
||||
name: Mastodon
|
||||
mediawiki:
|
||||
name: MediaWiki
|
||||
nostr:
|
||||
name: Nostr
|
||||
remotestorage:
|
||||
name: remoteStorage
|
||||
display_name: Storage
|
||||
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
|
||||
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.1].define(version: 2024_04_22_171653) do
|
||||
ActiveRecord::Schema[7.1].define(version: 2024_06_07_123654) do
|
||||
create_table "active_storage_attachments", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "record_type", null: false
|
||||
@@ -144,6 +144,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_04_22_171653) do
|
||||
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
|
||||
|
||||
|
||||
@@ -47,6 +47,9 @@ services:
|
||||
RS_REDIS_URL: redis://redis:6379/1
|
||||
RS_STORAGE_URL: "http://localhost:4567"
|
||||
S3_ENABLED: false
|
||||
NOSTR_PUBLIC_KEY: bdd76ce2934b2f591f9fad2ebe9da18f20d2921de527494ba00eeaa0a0efadcf
|
||||
NOSTR_PRIVATE_KEY: 7c3ef7e448505f0615137af38569d01807d3b05b5005d5ecf8aaafcd40323cea
|
||||
NOSTR_RELAY_URL: "ws://strfry:7777"
|
||||
depends_on:
|
||||
- ldap
|
||||
- redis
|
||||
@@ -107,16 +110,23 @@ services:
|
||||
- minio
|
||||
- redis
|
||||
|
||||
nostr-relay:
|
||||
image: pluja/strfry:latest
|
||||
strfry:
|
||||
image: gitea.kosmos.org/kosmos/strfry-deno:1.1.1
|
||||
volumes:
|
||||
- ./docker/strfry/strfry.conf:/etc/strfry.conf
|
||||
- strfry-data:/app/strfry-db
|
||||
- ./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:
|
||||
# image: osixia/phpldapadmin:0.9.0
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
##
|
||||
|
||||
# Directory that contains the strfry LMDB database (restart required)
|
||||
db = "./strfry-db/"
|
||||
db = "/var/lib/strfry/"
|
||||
|
||||
dbParams {
|
||||
# Maximum number of threads/processes that can simultaneously have LMDB transactions open (restart required)
|
||||
@@ -54,7 +54,7 @@ relay {
|
||||
|
||||
info {
|
||||
# NIP-11: Name of this server. Short/descriptive (< 30 characters)
|
||||
name = "akkounts-nostr-relay"
|
||||
name = "Akkounts Nostr Relay"
|
||||
|
||||
# NIP-11: Detailed information about relay, free-form
|
||||
description = "Local strfry instance for akkounts development"
|
||||
@@ -86,7 +86,7 @@ relay {
|
||||
|
||||
writePolicy {
|
||||
# If non-empty, path to an executable script that implements the writePolicy plugin logic
|
||||
plugin = ""
|
||||
plugin = "/opt/strfry/strfry-policy.ts"
|
||||
}
|
||||
|
||||
compression {
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
"postcss-preset-env": "^7.8.3",
|
||||
"tailwindcss": "^3.2.4"
|
||||
},
|
||||
"version": "0.9.0",
|
||||
"version": "0.10.0",
|
||||
"scripts": {
|
||||
"build:css:tailwind": "tailwindcss --postcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css",
|
||||
"build:css": "yarn run build:css:tailwind"
|
||||
|
||||
BIN
public/img/app_icons/hyperdraft.png
Normal file
BIN
public/img/app_icons/hyperdraft.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
public/img/app_icons/kommit.png
Normal file
BIN
public/img/app_icons/kommit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
public/img/app_icons/notes-together.png
Normal file
BIN
public/img/app_icons/notes-together.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
BIN
public/img/app_icons/papiers.png
Normal file
BIN
public/img/app_icons/papiers.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
public/img/app_icons/petrolette.png
Normal file
BIN
public/img/app_icons/petrolette.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user