105 Commits

Author SHA1 Message Date
78ecef80dc WIP: Import deno modules for use with import maps
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-09 15:17:35 +02:00
64de4deddd Fix serviceEnabled indicator on admin page
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-24 21:38:01 +02:00
8f7994d82e 0.10.0
All checks were successful
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2024-09-18 15:49:07 +02:00
a7d0e71ab6 Fix spec
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-18 14:46:46 +02:00
27d9f73c61 Set host for RS auth url
Some checks failed
continuous-integration/drone/push Build is failing
With X-Forwarded-Host set on the proxied request, Rails uses that host
for URLs. But we need it to be the accounts domain.
2024-09-14 17:17:09 +02:00
ed3de8b16f Allow CORS for all LNURL endpoints
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-14 16:46:14 +02:00
d7b4c67953 Fix config when set to empty string
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-14 16:40:22 +02:00
7489d4a32f Merge pull request 'Add config for separate primary domain Nostr pubkey' (#204) from feature/nostr_pubkey_primary_domain into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #204
Reviewed-by: Greg <greg@noreply.kosmos.org>
2024-09-13 12:33:11 +00:00
ac77e5b7c1 Allow ENV var for new setting
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 5s
2024-09-11 16:31:04 +02:00
e544c28105 Config for separate primary domain Nostr pubkey
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Allow to configure a separate key for the NIP-05 address of the primary
domain vs the accounts domain.
2024-09-11 16:28:12 +02:00
4909dac5c2 Fix typo
All checks were successful
continuous-integration/drone/push Build is passing
The return value of `strip!` is `nil`
2024-09-11 16:26:48 +02:00
3cf4348695 Merge pull request 'Make default user services configurable by admins' (#203) from feature/default_service_settings into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #203
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-09-11 11:21:38 +00:00
af3da0a26c Set CORS headers for all .well-known responses
All checks were successful
continuous-integration/drone/push Build is passing
So we don't have to consider it for reverse proxies etc.
2024-09-10 16:06:11 +02:00
2d32320c7d Style check boxes
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 5s
2024-09-05 11:24:38 +02:00
fc2bec6246 Make default user services configurable by admin
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-09-05 11:11:32 +02:00
5addd25186 Add service details config, use for known services 2024-09-05 11:10:54 +02:00
215d178e69 Remove empty spec files 2024-09-05 11:10:10 +02:00
5474bf66e7 Turn default services into a configurable setting
With the default value being all enabled services
2024-09-04 13:06:32 +02:00
ef2a37e2bf Sort user services in LDAP entry
Makes it predictable for programmatic comparisons (e.g. tests)
2024-09-04 13:05:36 +02:00
0e3180602c Rename "xmpp" user service back to "ejabberd"
If we ever add support for others, we can combine them as "xmpp" in
helper methods
2024-09-04 13:03:45 +02:00
15e2f9b962 Remove "in development" note
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-28 14:55:34 +02:00
4ae10c9b53 Refactor settings model
All checks were successful
continuous-integration/drone/push Build is passing
Move the various sections to their own concerns, so they're easier to
find and maintain
2024-08-28 14:39:08 +02:00
45137e0cfe Merge pull request 'Fix Ruby issue on Apple silicon (without compiling a patched Ruby)' (#201) from chore/update_docker_image into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #201
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-08-28 08:12:31 +00:00
717fe93104 Fix spec
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-22 14:07:54 +02:00
fdac789ccb Add compatibility section to RS service page
Some checks failed
continuous-integration/drone/push Build is failing
2024-08-19 15:13:19 +02:00
9355dab6b6 Enable RS service for all new users for now
Some checks failed
continuous-integration/drone/push Build is failing
2024-08-19 14:48:24 +02:00
f3676949d2 Fix redirect
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-17 14:49:19 +02:00
79952b73c5 Fix link descriptions
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-17 14:45:31 +02:00
17c419403e Merge pull request 'Finish MVP of remoteStorage service pages/UI' (#202) from feature/rs_service_page into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #202
Reviewed-by: Greg <greg@noreply.kosmos.org>
2024-08-17 12:33:48 +00:00
6d06312a5c Update manifique gem
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 6s
Fixes a bug with some manifest files
2024-08-14 18:07:27 +02:00
acb399b0b7 Add app recommendation for Notes Together
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2024-08-14 16:32:06 +02:00
bf20b6467e Re-order services on dashboard
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-08-14 13:37:22 +02:00
b91d90d75c Fix some specs, improve config
Allow empty string to unset nostr relay URL config
2024-08-14 13:37:15 +02:00
3284bbf6ca Add recommended apps for RS 2024-08-14 13:35:49 +02:00
171b84ee81 Add tabnav, dedicated auths view to RS service page
Includes a nicer view and illustration for when no auths exist yet
2024-08-14 13:35:02 +02:00
54b01dd282 Drive-by content update 2024-08-12 11:14:12 +02:00
e08ea64f47 Update Docker base image
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 5s
Fixes the bug with Ruby on Apple silicon
2024-08-12 10:34:02 +02:00
8cc2c9554f Revert "Fix Ruby in Docker container on Apple silicon"
This reverts commit bbf3fb91a0.
2024-08-12 10:15:18 +02:00
32dff9c67f Merge pull request 'Add dashboard icons for remoteStorage and email' (#200) from chore/dashboard_service_icons into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #200
2024-08-12 07:03:22 +00:00
126b8b20e0 Improve dashboard icon opacity, layout
All checks were successful
continuous-integration/drone Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 5s
2024-08-10 12:44:49 +02:00
5abf69f356 Add email service icon to dashboard 2024-08-10 12:44:25 +02:00
210a69bd9b Add Geary app recommendation to email page 2024-08-09 14:19:49 +02:00
bbed3cd367 Add RS logo to service grid, resize others 2024-08-09 12:37:18 +02:00
7943da0f17 Add note 2024-08-09 12:34:10 +02:00
620167eedf Merge pull request 'Admin pages: fix more user links, add missing services to user page' (#199) from feature/admin_pages into master
Reviewed-on: #199
2024-08-09 10:33:29 +00:00
e077debfc2 Use npub for njump link
All checks were successful
continuous-integration/drone/push Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 4s
2024-06-23 17:30:03 +02:00
531b2c3002 Fix more links 2024-06-23 17:29:48 +02:00
6d2bc729b8 Add new services to admin user page
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-23 17:26:33 +02:00
2630ec2af4 Fix admin user links
All checks were successful
continuous-integration/drone/push Build is passing
refs #166
2024-06-23 17:24:48 +02:00
daed5c1eea Merge pull request 'Allow non-members to publish zap receipts for members' (#197) from feature/strfry_zap_receipts into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #197
Reviewed-by: bumi <bumi@noreply.kosmos.org>
2024-06-22 17:52:03 +00:00
2e9429bb32 Merge pull request 'Add support for integrated Nostr relay service' (#198) from feature/own_relay into feature/strfry_zap_receipts
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 6s
Reviewed-on: #198
Reviewed-by: bumi <bumi@noreply.kosmos.org>
2024-06-22 17:51:40 +00:00
37c15c7a62 Check in deno lockfile
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 6s
2024-06-20 15:51:40 +02:00
01ecea74ff Add pubkey whitelist to strfry policy
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
And allow the local akkounts instance to publish on the local relay
2024-06-20 15:28:17 +02:00
f401a03590 Fix exception for NIP-05 JSON of "_" with relay configured
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-06-20 14:50:02 +02:00
fff6dea100 Add support for placeholder attribute to component
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-20 13:54:59 +02:00
48ab96dda9 Support "_" placeholder username for domain's own NIP-05
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-06-19 20:57:22 +02:00
7ac3130c18 Consistent formatting
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-19 20:31:31 +02:00
cbfa148051 Publish zap receipts to own relay in addition to requested ones
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-19 20:26:24 +02:00
87d900b627 Add own relay to NIP-05 relay list if configured 2024-06-19 20:06:07 +02:00
926dc06294 Add global setting for own nostr relay 2024-06-19 19:57:09 +02:00
00b73b06d7 Remove obsolete variable
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-06-19 15:56:45 +02:00
0daac33915 Allow non-members to publish zap receipts for members
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-06-19 15:43:56 +02:00
0e472bc311 Improve strfry extras usage 2024-06-19 15:43:24 +02:00
40b34d0935 Merge pull request 'Add strfry policies and members-only LDAP policy' (#196) from feature/strfry_policies into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #196
2024-06-11 20:10:34 +00:00
61cb8f4941 Add script for syncing notes from remote relays
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 4s
2024-06-11 22:06:51 +02:00
433ac4dc8e Use new strfry Docker image 2024-06-11 22:06:12 +02:00
62fe0d8fac Add nostrKey to default org service ACI 2024-06-11 22:05:07 +02:00
2a675fd135 Hand LDAP config to policy from main policy file
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Deployments will differ in production. The policy itself just needs the
configs, but should not care where credentials are fetched from.
2024-06-09 23:15:56 +02:00
c2c3ebc2e1 Add strfry policies and members-only LDAP policy
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
This will look up nostr pubkeys in the LDAP directory to allow or deny
publishing notes to the relay.
2024-06-09 22:49:44 +02:00
5a5c316c14 Fix time format in migration
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-09 13:29:39 +02:00
f0d5457ec1 Merge pull request 'Zap model improvements' (#195) from chore/zap_model_improvements into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #195
Reviewed-by: bumi <bumi@noreply.kosmos.org>
2024-06-09 11:16:34 +00:00
5588e3b3e8 Add settled_at to zaps, scope by settlement status
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 4s
2024-06-07 15:11:06 +02:00
8949d76d26 Fix zap receipt not being stored correctly
All checks were successful
continuous-integration/drone/push Build is passing
fixes #194
2024-06-07 13:40:49 +02:00
8bc9bbdc33 Merge pull request 'Add new Lightning notification settings' (#193) from feature/ln_notification_settings into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #193
Reviewed-by: bumi <bumi@noreply.kosmos.org>
2024-06-04 10:39:07 +00:00
d6d09b57b8 Merge pull request 'Add support for Lightning Zaps' (#190) from feature/170-nostr_zaps into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #190
2024-06-03 16:44:48 +00:00
1685d6ecf8 Respect new Lightning notification settings
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 4s
2024-06-01 17:51:20 +02:00
5348a229a6 WIP Add new lightning notification settings 2024-05-29 15:12:07 +01:00
bad3b7a2be Use dynamic list for allowed user preference params 2024-05-23 00:23:42 +02:00
b541e95bb7 Change default for lightning notifications 2024-05-23 00:22:38 +02:00
3f43fe8101 Fix missing description for FieldsetToggleComponent 2024-05-23 00:01:25 +02:00
231dfc8404 Log correct publish status
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 4s
2024-05-21 18:28:46 +02:00
eeb9b0a331 Improve NostrManager::PublishEvent
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
* Use URI hostname as relay name
* Log relay name/URL for every websocket event
* Fix variable assignment for nostr event
* Fix Sidekiq job finishing too early, by creating a new thread waiting
  for it to be closed from a callback
2024-05-21 18:08:14 +02:00
08e783d185 Remove default nil values
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-19 17:07:27 +02:00
fa5dc8ca46 Fix argument name
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-19 16:54:51 +02:00
bc34e9c5e0 Allow CORS requests for lnurlp invoice
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-19 16:48:09 +02:00
f388bd0237 Merge branch 'master' into feature/170-nostr_zaps
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-10 12:01:27 +00:00
48041630ca Limit number of relays to publish zap receipts to
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-10 13:57:25 +02:00
2d1ff29eca Improve nostr settings, fix allowsNostr property name
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-10 13:19:09 +02:00
c6c5d80fb4 WIP Persist zaps, create and send zap receipts
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-09 14:31:37 +02:00
c0f4e7925e Use zap comment for description/memo
Some checks failed
continuous-integration/drone/push Build is failing
But use the hashed zap request event for the description hash.
2024-05-04 17:07:23 +02:00
49d24990b4 Add zap model, user relation 2024-05-04 17:05:34 +02:00
619bd954b7 WIP 2024-04-21 10:51:41 +02:00
e27c64b5f1 WIP Check for zaps, send zap receipt on incoming zap tx 2024-04-21 10:35:30 +02:00
b36baf26eb Refactor WebhooksController 2024-04-21 10:02:17 +02:00
adedaa5f7b Add task for easily creating test invoices 2024-04-21 10:01:54 +02:00
596ed7fccc Use lndhub.go v2 endpoint for invoice creation 2024-04-21 10:01:18 +02:00
5685e1b7bc Move lndhub invoice creation to service 2024-04-16 20:19:15 +02:00
c3b82fc2a9 WIP Verify and respond to zap requests
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-16 19:13:10 +02:00
77e2fe5792 Add helper method for parsing nostr event tags 2024-04-16 19:10:48 +02:00
bc43082839 Add admin settings for nostr keys 2024-04-16 19:07:52 +02:00
b09225543b Add Nostr relay service to Docker Compose config 2024-04-15 14:03:37 +02:00
f2507409a3 Announce nostr pubkey on lnurlp endpoint 2024-04-15 14:03:37 +02:00
46b4723999 Add global settings for account service's Nostr keys 2024-04-15 14:03:37 +02:00
3f90a011c4 Document URLs 2024-04-15 14:03:37 +02:00
3ba333e802 Indentation 2024-04-15 14:03:37 +02:00
123 changed files with 3070 additions and 441 deletions

View File

@@ -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'

View File

@@ -18,6 +18,9 @@ LNDHUB_API_URL='http://localhost:3026'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
NOSTR_PRIVATE_KEY='7c3ef7e448505f0615137af38569d01807d3b05b5005d5ecf8aaafcd40323cea'
NOSTR_PUBLIC_KEY='bdd76ce2934b2f591f9fad2ebe9da18f20d2921de527494ba00eeaa0a0efadcf'
RS_STORAGE_URL='https://storage.kosmos.org'
RS_REDIS_URL='redis://localhost:6379/1'

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
@jsr:registry=https://npm.jsr.io

View File

@@ -1,24 +1,17 @@
# 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
WORKDIR /akkounts
COPY ["Gemfile", "Gemfile.lock", "package.json", "./"]
COPY ["Gemfile", "Gemfile.lock", "package.json", "yarn.lock", "./"]
RUN bundle install
RUN gem install foreman

View File

@@ -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

View File

@@ -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)
@@ -542,4 +542,4 @@ DEPENDENCIES
webmock
BUNDLED WITH
2.5.5
2.5.11

View File

@@ -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%);
}
}

View File

@@ -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]
},

View File

@@ -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)

View File

@@ -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">

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,13 +1,15 @@
class LnurlpayController < ApplicationController
before_action :check_service_available
before_action :find_user
before_action :set_cors_access_control_headers
MIN_SATS = 10
MAX_SATS = 1_000_000
MAX_COMMENT_CHARS = 100
# GET /.well-known/lnurlp/:username
def index
render json: {
res = {
status: "OK",
callback: "https://#{Setting.accounts_domain}/lnurlpay/#{@user.cn}/invoice",
tag: "payRequest",
@@ -16,8 +18,16 @@ class LnurlpayController < ApplicationController
metadata: metadata(@user.address),
commentAllowed: MAX_COMMENT_CHARS
}
if Setting.nostr_enabled?
res[:allowsNostr] = true
res[:nostrPubkey] = Setting.nostr_public_key
end
render json: res
end
# GET /.well-known/keysend/:username
def keysend
http_status :not_found and return unless Setting.lndhub_keysend_enabled?
@@ -32,8 +42,9 @@ class LnurlpayController < ApplicationController
}
end
# GET /lnurlpay/:username/invoice
def invoice
amount = params[:amount].to_i / 1000 # msats
amount = params[:amount].to_i / 1000 # msats to sats
comment = params[:comment] || ""
address = @user.address
@@ -42,53 +53,109 @@ class LnurlpayController < ApplicationController
return
end
if !valid_comment?(comment)
render json: { status: "ERROR", reason: "Comment too long" }
return
if params[:nostr].present? && Setting.nostr_enabled?
handle_zap_request amount, params[:nostr], params[:lnurl]
else
handle_pay_request address, amount, comment
end
end
private
def set_cors_access_control_headers
headers['Access-Control-Allow-Origin'] = "*"
headers['Access-Control-Allow-Headers'] = "*"
headers['Access-Control-Allow-Methods'] = "GET"
end
memo = "To #{address}"
memo = "#{memo}: \"#{comment}\"" if comment.present?
def check_service_available
http_status :not_found unless Setting.lndhub_enabled?
end
payment_request = @user.ln_create_invoice({
amount: amount, # we create invoices in sats
memo: memo,
description_hash: Digest::SHA2.hexdigest(metadata(address)),
})
def find_user
@user = User.where(cn: params[:username], ou: Setting.primary_domain).first
http_status :not_found if @user.nil?
end
render json: {
status: "OK",
successAction: {
tag: "message",
message: "Sats received. Thank you!"
},
routes: [],
pr: payment_request
}
end
def metadata(address)
"[[\"text/identifier\",\"#{address}\"],[\"text/plain\",\"Sats for #{address}\"]]"
end
private
def valid_amount?(amount_in_sats)
amount_in_sats <= MAX_SATS && amount_in_sats >= MIN_SATS
end
def find_user
@user = User.where(cn: params[:username], ou: Setting.primary_domain).first
http_status :not_found if @user.nil?
end
def valid_comment?(comment)
comment.length <= MAX_COMMENT_CHARS
end
def metadata(address)
"[[\"text/identifier\", \"#{address}\"], [\"text/plain\", \"Send sats, receive thanks.\"]]"
end
def handle_pay_request(address, amount, comment)
if !valid_comment?(comment)
render json: { status: "ERROR", reason: "Comment too long" }
return
end
def valid_amount?(amount_in_sats)
amount_in_sats <= MAX_SATS && amount_in_sats >= MIN_SATS
end
desc = "To #{address}"
desc = "#{desc}: \"#{comment}\"" if comment.present?
def valid_comment?(comment)
comment.length <= MAX_COMMENT_CHARS
end
invoice = LndhubManager::CreateUserInvoice.call(
user: @user, payload: {
amount: amount, # sats
description: desc,
description_hash: Digest::SHA256.hexdigest(metadata(address)),
}
)
private
render json: {
status: "OK",
successAction: {
tag: "message",
message: "Sats received. Thank you!"
},
routes: [],
pr: invoice["payment_request"]
}
end
def check_service_available
http_status :not_found unless Setting.lndhub_enabled?
end
def nostr_event_from_payload(nostr_param)
event_obj = JSON.parse(nostr_param).transform_keys(&:to_sym)
Nostr::Event.new(**event_obj)
rescue => e
return nil
end
def valid_zap_request?(amount, event, lnurl)
NostrManager::VerifyZapRequest.call(
amount: amount, event: event, lnurl: lnurl
)
end
def handle_zap_request(amount, nostr_param, lnurl_param)
event = nostr_event_from_payload(nostr_param)
unless event.present? && valid_zap_request?(amount*1000, event, lnurl_param)
render json: { status: "ERROR", reason: "Invalid zap request" }
return
end
# TODO might want to use the existing invoice and zap record if there are
# multiple calls with the same zap request
desc = "Zap for #{@user.address}"
desc = "#{desc}: \"#{event.content}\"" if event.content.present?
invoice = LndhubManager::CreateUserInvoice.call(
user: @user, payload: {
amount: amount, # sats
description: desc,
description_hash: Digest::SHA256.hexdigest(event.to_json),
}
)
@user.zaps.create! request: event,
payment_request: invoice["payment_request"],
amount: amount
render json: { status: "OK", pr: invoice["payment_request"] }
end
end

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -12,7 +12,11 @@ class SettingsController < ApplicationController
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
@@ -147,11 +151,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

View File

@@ -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

View File

@@ -2,45 +2,76 @@ class WebhooksController < ApplicationController
skip_forgery_protection
before_action :authorize_request
before_action :process_payload
def lndhub
begin
payload = JSON.parse(request.body.read, symbolize_names: true)
head :no_content and return unless payload[:type] == "incoming"
rescue
head :unprocessable_entity and return
@user = User.find_by!(ln_account: @payload[:user_login])
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: settled_at.to_i,
preimage: @payload[:preimage]
)
@zap.update! settled_at: settled_at, receipt: zap_receipt.to_h
NostrManager::PublishZapReceipt.call(zap: @zap)
end
user = User.find_by!(ln_account: payload[:user_login])
notify = user.preferences[:lightning_notify_sats_received]
case notify
when "xmpp"
notify_xmpp(user.address, payload[:amount], payload[:memo])
when "email"
NotificationMailer.with(user: user, amount_sats: payload[:amount])
.lightning_sats_received.deliver_later
end
send_notifications
head :ok
end
private
# TODO refactor into mailer-like generic class/service
def notify_xmpp(address, amt_sats, memo)
payload = {
type: "normal",
from: Setting.xmpp_notifications_from_address,
to: address,
subject: "Sats received!",
body: "#{helpers.number_with_delimiter amt_sats} sats received in your Lightning wallet:\n> #{memo}"
}
XmppSendMessageJob.perform_later(payload)
end
def authorize_request
if !ENV['WEBHOOKS_ALLOWED_IPS'].split(',').include?(request.remote_ip)
head :forbidden and return
end
end
def process_payload
@payload = JSON.parse(request.body.read, symbolize_names: true)
unless @payload[:type] == "incoming" &&
@payload[:state] == "settled"
head :no_content and return
end
rescue
head :unprocessable_entity and return
end
def send_notifications
return if @payload[:amount] < @user.preferences[:lightning_notify_min_sats]
if @user.preferences[:lightning_notify_only_with_message]
return if @payload[:memo].blank?
end
target = @zap.present? ? @user.preferences[:lightning_notify_zap_received] :
@user.preferences[:lightning_notify_sats_received]
case target
when "xmpp"
notify_xmpp
when "email"
notify_email
end
end
# TODO refactor into mailer-like generic class/service
def notify_xmpp
XmppSendMessageJob.perform_later({
type: "normal",
from: Setting.xmpp_notifications_from_address,
to: @user.address,
subject: "Sats received!",
body: "#{helpers.number_with_delimiter @payload[:amount]} sats received in your Lightning wallet:\n> #{@payload[:memo]}"
})
end
def notify_email
NotificationMailer.with(user: @user, amount_sats: @payload[:amount])
.lightning_sats_received.deliver_later
end
end

View File

@@ -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

View File

@@ -1,2 +0,0 @@
module DashboardHelper
end

View File

@@ -1,2 +0,0 @@
module DonationsHelper
end

View File

@@ -1,2 +0,0 @@
module InvitationsHelper
end

View File

@@ -1,2 +0,0 @@
module LnurlpayHelper
end

View 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

View File

@@ -1,2 +0,0 @@
module SettingsHelper
end

View File

@@ -1,2 +0,0 @@
module SignupHelper
end

View File

@@ -1,2 +0,0 @@
module UsersHelper
end

View File

@@ -1,2 +0,0 @@
module WalletHelper
end

View File

@@ -1,2 +0,0 @@
module WelcomeHelper
end

View File

@@ -1,4 +1,5 @@
import { Controller } from "@hotwired/stimulus"
// import { Nostrify } from '@nostrify/nostrify';
// Connects to data-controller="nostr-login"
export default class extends Controller {
@@ -6,6 +7,9 @@ export default class extends Controller {
static values = { site: String, sharedSecret: String }
connect() {
// window.Nostrify = Nostrify;
// console.log(Nostrify);
if (window.nostr) {
this.loginButtonTarget.disabled = false
this.loginFormTarget.classList.remove("hidden")

View File

@@ -0,0 +1,7 @@
class NostrPublishEventJob < ApplicationJob
queue_as :nostr
def perform(event:, relay_url:)
NostrManager::PublishEvent.call(event: event, relay_url: relay_url)
end
end

View File

@@ -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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,25 @@
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
end
end
end

View File

@@ -0,0 +1,9 @@
module Settings
module OpenCollectiveSettings
extend ActiveSupport::Concern
included do
field :opencollective_enabled, type: :boolean, default: true
end
end
end

View 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

View 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

View File

@@ -2,213 +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: false
#
# 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

View File

@@ -17,16 +17,15 @@ class User < ApplicationRecord
has_one :invitation, inverse_of: :invitee, foreign_key: 'invited_user_id'
has_one :inviter, through: :invitation, source: :user
has_many :invitees, through: :invitations
has_many :donations, dependent: :nullify
has_many :remote_storage_authorizations
has_many :zaps
has_one :lndhub_user, class_name: "LndhubUser", inverse_of: "user",
primary_key: "ln_account", foreign_key: "login"
has_many :accounts, through: :lndhub_user
has_many :remote_storage_authorizations
#
# Validations
#
@@ -143,12 +142,6 @@ class User < ApplicationRecord
enable_service Setting.default_services
end
def ln_create_invoice(payload)
lndhub = Lndhub.new
lndhub.authenticate self
lndhub.addinvoice payload
end
def dn
return @dn if defined?(@dn)
@dn = Devise::LDAP::Adapter.get_dn(self.cn)
@@ -187,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

View File

@@ -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

20
app/models/zap.rb Normal file
View File

@@ -0,0 +1,20 @@
class Zap < ApplicationRecord
belongs_to :user
scope :settled, -> { where.not(settled_at: nil) }
scope :unpaid, -> { where(settled_at: nil) }
def request_event
nostr_event_from_hash(request)
end
def receipt_event
nostr_event_from_hash(receipt)
end
private
def nostr_event_from_hash(hash)
Nostr::Event.new(**hash.symbolize_keys)
end
end

View File

@@ -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 = {

View File

@@ -0,0 +1,13 @@
module LndhubManager
class CreateUserInvoice < LndhubV2
def initialize(user:, payload:)
@user = user
@payload = payload
end
def call
authenticate @user
create_invoice @payload
end
end
end

View File

@@ -0,0 +1,25 @@
module NostrManager
class CreateZapReceipt < NostrManagerService
def initialize(zap:, paid_at:, preimage:)
@zap, @paid_at, @preimage = zap, paid_at, preimage
end
def call
request_tags = parse_tags(@zap.request_event.tags)
site_user.create_event(
kind: 9735,
created_at: @paid_at,
content: "",
tags: [
["p", request_tags[:p].first],
["e", request_tags[:e]&.first],
["a", request_tags[:a]&.first],
["bolt11", @zap.payment_request],
["preimage", @preimage],
["description", @zap.request_event.to_json]
].reject { |t| t[1].nil? }
)
end
end
end

View File

@@ -0,0 +1,50 @@
module NostrManager
class PublishEvent < NostrManagerService
def initialize(event:, relay_url:)
relay_name = URI.parse(relay_url).host
@relay = Nostr::Relay.new(url: relay_url, name: relay_name)
if event.is_a?(Nostr::Event)
@event = event
else
@event = Nostr::Event.new(**event.symbolize_keys)
end
@client = Nostr::Client.new
end
def call
client, relay, event = @client, @relay, @event
log_prefix = "[nostr][#{relay.name}]"
thread = Thread.new do
client.on :connect do
puts "#{log_prefix} Publishing #{event.id}..."
client.publish event
end
client.on :error do |e|
puts "#{log_prefix} Error: #{e}"
puts "#{log_prefix} Closing thread..."
thread.exit
end
client.on :message do |m|
puts "#{log_prefix} Message: #{m}"
msg = JSON.parse(m) rescue []
if msg[0] == "OK" && msg[1] == event.id && msg[2]
puts "#{log_prefix} Event published. Closing thread..."
else
puts "#{log_prefix} Unexpected message from relay. Closing thread..."
end
thread.exit
end
puts "#{log_prefix} Connecting to #{relay.url}..."
client.connect relay
end
thread.join
end
end
end

View File

@@ -0,0 +1,24 @@
module NostrManager
class PublishZapReceipt < NostrManagerService
def initialize(zap:, delayed: true)
@zap, @delayed = zap, delayed
end
def call
tags = parse_tags(@zap.request_event.tags)
relays = tags[:relays].take(Setting.nostr_zaps_relay_limit)
if Setting.nostr_relay_url.present?
relays << Setting.nostr_relay_url
end
relays.uniq.each do |relay_url|
if @delayed
NostrPublishEventJob.perform_later(event: @zap.receipt, relay_url: relay_url)
else
NostrManager::PublishEvent.call(event: @zap.receipt_event, relay_url: relay_url)
end
end
end
end
end

View File

@@ -7,8 +7,9 @@ module NostrManager
end
def call
site_given = @event.tags.find{|t| t[0] == "site"}[1]
challenge_given = @event.tags.find{|t| t[0] == "challenge"}[1]
tags = parse_tags(@event.tags)
site_given = tags[:site].first
challenge_given = tags[:challenge].first
site_given == @site_expected &&
challenge_given == @challenge_expected

View File

@@ -0,0 +1,51 @@
module NostrManager
class VerifyZapRequest < NostrManagerService
def initialize(amount:, event:, lnurl: nil)
@amount, @event, @lnurl = amount, event, lnurl
end
# https://github.com/nostr-protocol/nips/blob/27fef638e2460139cc9078427a0aec0ce4470517/57.md#appendix-d-lnurl-server-zap-request-validation
def call
tags = parse_tags(@event.tags)
@event.verify_signature &&
@event.kind == 9734 &&
tags.present? &&
valid_p_tag?(tags[:p]) &&
valid_e_tag?(tags[:e]) &&
valid_a_tag?(tags[:a]) &&
valid_amount_tag?(tags[:amount]) &&
valid_lnurl_tag?(tags[:lnurl])
end
def valid_p_tag?(tag)
return false unless tag.present? && tag.length == 1
key = Nostr::PublicKey.new(tag.first) rescue nil
key.present?
end
def valid_e_tag?(tag)
return true unless tag.present?
# TODO validate format of event ID properly
tag.length == 1 && tag.first.is_a?(String)
end
def valid_a_tag?(tag)
return true unless tag.present?
# TODO validate format of event coordinate properly
tag.length == 1 && tag.first.is_a?(String)
end
def valid_amount_tag?(tag)
return true unless tag.present?
amount = tag.first
amount.is_a?(String) && amount.to_i == @amount
end
def valid_lnurl_tag?(tag)
return true unless tag.present?
# TODO validate lnurl matching recipient's lnurlp
tag.first.is_a?(String)
end
end
end

View File

@@ -1,4 +1,22 @@
require "nostr"
class NostrManagerService < ApplicationService
def parse_tags(tags)
out = {}
tags.each do |tag|
out[tag[0].to_sym] = tag[1, tag.length]
end
out
end
def site_keypair
Nostr::KeyPair.new(
private_key: Nostr::PrivateKey.new(Setting.nostr_private_key),
public_key: Nostr::PublicKey.new(Setting.nostr_public_key)
)
end
def site_user
Nostr::User.new(keypair: site_keypair)
end
end

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -7,4 +7,37 @@
title: "Enable Nostr integration (experimental)",
description: "Allow adding nostr pubkeys and resolve user addresses via NIP-05"
) %>
<% if Setting.nostr_enabled? %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :nostr_private_key,
type: :password,
title: "Private key",
description: "The private key of the accounts service, used when publishing events (e.g. zap receipts)"
) %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :nostr_public_key,
title: "Public key",
description: "The corresponding public key of the accounts service"
) %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :nostr_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"
) %>
</ul>
<% end %>

View File

@@ -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,

View File

@@ -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 %>&mdash;<% 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 %>&mdash;<% 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>

View File

@@ -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 %>

View File

@@ -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 %>

View File

@@ -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">

View File

@@ -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 %>

View 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 %>

View File

@@ -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>

View 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>

View File

@@ -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

View 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

View File

@@ -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

30
config/services.yml Normal file
View 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

View File

@@ -3,3 +3,4 @@
- default
- mailers
- remotestorage
- nostr

View File

@@ -0,0 +1,13 @@
class CreateZaps < ActiveRecord::Migration[7.1]
def change
create_table :zaps do |t|
t.references :user, null: false, foreign_key: true
t.json :request
t.json :receipt, default: nil
t.text :payment_request
t.bigint :amount
t.timestamps
end
end
end

View 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

View File

@@ -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_03_16_153558) 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
@@ -136,8 +136,21 @@ ActiveRecord::Schema[7.1].define(version: 2024_03_16_153558) do
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
create_table "zaps", force: :cascade do |t|
t.integer "user_id", null: false
t.json "request"
t.json "receipt"
t.text "payment_request"
t.bigint "amount"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "settled_at"
t.index ["user_id"], name: "index_zaps_on_user_id"
end
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "remote_storage_authorizations", "app_catalog_web_apps", column: "web_app_id"
add_foreign_key "remote_storage_authorizations", "users"
add_foreign_key "zaps", "users"
end

245
deno.lock generated Normal file
View File

@@ -0,0 +1,245 @@
{
"version": "3",
"packages": {
"specifiers": {
"jsr:@deno/cache-dir@0.8": "jsr:@deno/cache-dir@0.8.0",
"jsr:@deno/emit": "jsr:@deno/emit@0.45.0",
"jsr:@luca/esbuild-deno-loader@0.9": "jsr:@luca/esbuild-deno-loader@0.9.0",
"jsr:@std/assert@^0.213.1": "jsr:@std/assert@0.213.1",
"jsr:@std/assert@^0.218.2": "jsr:@std/assert@0.218.2",
"jsr:@std/assert@^0.223.0": "jsr:@std/assert@0.223.0",
"jsr:@std/bytes@^0.218.2": "jsr:@std/bytes@0.218.2",
"jsr:@std/encoding@0.213": "jsr:@std/encoding@0.213.1",
"jsr:@std/fmt@^0.218.2": "jsr:@std/fmt@0.218.2",
"jsr:@std/fs@^0.218.2": "jsr:@std/fs@0.218.2",
"jsr:@std/io@^0.218.2": "jsr:@std/io@0.218.2",
"jsr:@std/jsonc@0.213": "jsr:@std/jsonc@0.213.1",
"jsr:@std/path@0.213": "jsr:@std/path@0.213.1",
"jsr:@std/path@^0.218.2": "jsr:@std/path@0.218.2",
"jsr:@std/path@^0.223.0": "jsr:@std/path@0.223.0",
"npm:esbuild@0.20": "npm:esbuild@0.20.2"
},
"jsr": {
"@deno/cache-dir@0.8.0": {
"integrity": "e87e80a404958f6350d903e6238b72afb92468378b0b32111f7a1e4916ac7fe7",
"dependencies": [
"jsr:@std/fmt@^0.218.2",
"jsr:@std/fs@^0.218.2",
"jsr:@std/io@^0.218.2",
"jsr:@std/path@^0.218.2"
]
},
"@deno/emit@0.45.0": {
"integrity": "b59d632e61dbe4be7e9e61235f02ad08ff124c714c31deb080c4de778da1894d",
"dependencies": [
"jsr:@deno/cache-dir@0.8",
"jsr:@std/path@^0.223.0"
]
},
"@luca/esbuild-deno-loader@0.9.0": {
"integrity": "288bbcede5c8a6f97e635f8fa4df779b13440ee0c0506d9e478fb6537789dc93",
"dependencies": [
"jsr:@std/encoding@0.213",
"jsr:@std/jsonc@0.213",
"jsr:@std/path@0.213",
"npm:esbuild@0.20"
]
},
"@std/assert@0.213.1": {
"integrity": "24c28178b30c8e0782c18e8e94ea72b16282207569cdd10ffb9d1d26f2edebfe"
},
"@std/assert@0.218.2": {
"integrity": "7f0a5a1a8cf86607cd6c2c030584096e1ffad27fc9271429a8cb48cfbdee5eaf"
},
"@std/assert@0.223.0": {
"integrity": "eb8d6d879d76e1cc431205bd346ed4d88dc051c6366365b1af47034b0670be24"
},
"@std/bytes@0.218.2": {
"integrity": "91fe54b232dcca73856b79a817247f4a651dbb60d51baafafb6408c137241670"
},
"@std/encoding@0.213.1": {
"integrity": "fcbb6928713dde941a18ca5db88ca1544d0755ec8fb20fe61e2dc8144b390c62"
},
"@std/fmt@0.218.2": {
"integrity": "99526449d2505aa758b6cbef81e7dd471d8b28ec0dcb1491d122b284c548788a"
},
"@std/fs@0.218.2": {
"integrity": "dd9431453f7282e8c577cc22c9e6d036055a9a980b5549f887d6012969fabcca"
},
"@std/io@0.218.2": {
"integrity": "c64fbfa087b7c9d4d386c5672f291f607d88cb7d44fc299c20c713e345f2785f",
"dependencies": [
"jsr:@std/assert@^0.218.2",
"jsr:@std/bytes@^0.218.2"
]
},
"@std/jsonc@0.213.1": {
"integrity": "5578f21aa583b7eb7317eed077ffcde47b294f1056bdbb9aacec407758637bfe",
"dependencies": [
"jsr:@std/assert@^0.213.1"
]
},
"@std/path@0.213.1": {
"integrity": "f187bf278a172752e02fcbacf6bd78a335ed320d080a7ed3a5a59c3e88abc673",
"dependencies": [
"jsr:@std/assert@^0.213.1"
]
},
"@std/path@0.218.2": {
"integrity": "b568fd923d9e53ad76d17c513e7310bda8e755a3e825e6289a0ce536404e2662",
"dependencies": [
"jsr:@std/assert@^0.218.2"
]
},
"@std/path@0.223.0": {
"integrity": "593963402d7e6597f5a6e620931661053572c982fc014000459edc1f93cc3989",
"dependencies": [
"jsr:@std/assert@^0.223.0"
]
}
},
"npm": {
"@esbuild/aix-ppc64@0.20.2": {
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
"dependencies": {}
},
"@esbuild/android-arm64@0.20.2": {
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
"dependencies": {}
},
"@esbuild/android-arm@0.20.2": {
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
"dependencies": {}
},
"@esbuild/android-x64@0.20.2": {
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
"dependencies": {}
},
"@esbuild/darwin-arm64@0.20.2": {
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
"dependencies": {}
},
"@esbuild/darwin-x64@0.20.2": {
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
"dependencies": {}
},
"@esbuild/freebsd-arm64@0.20.2": {
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
"dependencies": {}
},
"@esbuild/freebsd-x64@0.20.2": {
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
"dependencies": {}
},
"@esbuild/linux-arm64@0.20.2": {
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
"dependencies": {}
},
"@esbuild/linux-arm@0.20.2": {
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
"dependencies": {}
},
"@esbuild/linux-ia32@0.20.2": {
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
"dependencies": {}
},
"@esbuild/linux-loong64@0.20.2": {
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
"dependencies": {}
},
"@esbuild/linux-mips64el@0.20.2": {
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
"dependencies": {}
},
"@esbuild/linux-ppc64@0.20.2": {
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
"dependencies": {}
},
"@esbuild/linux-riscv64@0.20.2": {
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
"dependencies": {}
},
"@esbuild/linux-s390x@0.20.2": {
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
"dependencies": {}
},
"@esbuild/linux-x64@0.20.2": {
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
"dependencies": {}
},
"@esbuild/netbsd-x64@0.20.2": {
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
"dependencies": {}
},
"@esbuild/openbsd-x64@0.20.2": {
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
"dependencies": {}
},
"@esbuild/sunos-x64@0.20.2": {
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
"dependencies": {}
},
"@esbuild/win32-arm64@0.20.2": {
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
"dependencies": {}
},
"@esbuild/win32-ia32@0.20.2": {
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
"dependencies": {}
},
"@esbuild/win32-x64@0.20.2": {
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
"dependencies": {}
},
"esbuild@0.20.2": {
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
"dependencies": {
"@esbuild/aix-ppc64": "@esbuild/aix-ppc64@0.20.2",
"@esbuild/android-arm": "@esbuild/android-arm@0.20.2",
"@esbuild/android-arm64": "@esbuild/android-arm64@0.20.2",
"@esbuild/android-x64": "@esbuild/android-x64@0.20.2",
"@esbuild/darwin-arm64": "@esbuild/darwin-arm64@0.20.2",
"@esbuild/darwin-x64": "@esbuild/darwin-x64@0.20.2",
"@esbuild/freebsd-arm64": "@esbuild/freebsd-arm64@0.20.2",
"@esbuild/freebsd-x64": "@esbuild/freebsd-x64@0.20.2",
"@esbuild/linux-arm": "@esbuild/linux-arm@0.20.2",
"@esbuild/linux-arm64": "@esbuild/linux-arm64@0.20.2",
"@esbuild/linux-ia32": "@esbuild/linux-ia32@0.20.2",
"@esbuild/linux-loong64": "@esbuild/linux-loong64@0.20.2",
"@esbuild/linux-mips64el": "@esbuild/linux-mips64el@0.20.2",
"@esbuild/linux-ppc64": "@esbuild/linux-ppc64@0.20.2",
"@esbuild/linux-riscv64": "@esbuild/linux-riscv64@0.20.2",
"@esbuild/linux-s390x": "@esbuild/linux-s390x@0.20.2",
"@esbuild/linux-x64": "@esbuild/linux-x64@0.20.2",
"@esbuild/netbsd-x64": "@esbuild/netbsd-x64@0.20.2",
"@esbuild/openbsd-x64": "@esbuild/openbsd-x64@0.20.2",
"@esbuild/sunos-x64": "@esbuild/sunos-x64@0.20.2",
"@esbuild/win32-arm64": "@esbuild/win32-arm64@0.20.2",
"@esbuild/win32-ia32": "@esbuild/win32-ia32@0.20.2",
"@esbuild/win32-x64": "@esbuild/win32-x64@0.20.2"
}
}
}
},
"remote": {
"https://deno.land/x/denoflate@1.2.1/mod.ts": "f5628e44b80b3d80ed525afa2ba0f12408e3849db817d47a883b801f9ce69dd6",
"https://deno.land/x/denoflate@1.2.1/pkg/denoflate.js": "b9f9ad9457d3f12f28b1fb35c555f57443427f74decb403113d67364e4f2caf4",
"https://deno.land/x/denoflate@1.2.1/pkg/denoflate_bg.wasm.js": "d581956245407a2115a3d7e8d85a9641c032940a8e810acbd59ca86afd34d44d",
"https://deno.land/x/esbuild@v0.20.1/mod.js": "d50e500b53ce67e31116beba3916b0f9275c0e1cc20bc5cadc0fc1b7a3b06fd9"
},
"workspace": {
"packageJson": {
"dependencies": [
"npm:@jsr/nostrify__nostrify",
"npm:@tailwindcss/forms@^0.5.3",
"npm:autoprefixer@^10.4.13",
"npm:postcss-flexbugs-fixes@^5.0.2",
"npm:postcss-import@^15.0.1",
"npm:postcss-nested@^6.0.0",
"npm:postcss-preset-env@^7.8.3",
"npm:postcss@^8.4.19",
"npm:tailwindcss@^3.2.4"
]
}
}
}

View File

@@ -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,6 +110,24 @@ services:
- minio
- redis
strfry:
image: gitea.kosmos.org/kosmos/strfry-deno:1.1.1
volumes:
- ./docker/strfry/strfry.conf:/etc/strfry.conf
- ./extras/strfry:/opt/strfry
- strfry-data:/var/lib/strfry
networks:
- external_network
- internal_network
ports:
- "4777:7777"
environment:
LDAP_URL: 'ldap://ldap:3389'
LDAP_BIND_DN: 'cn=Directory Manager'
LDAP_PASSWORD: passthebutter
LDAP_SEARCH_DN: 'ou=kosmos.org,cn=users,dc=kosmos,dc=org'
WHITELIST_PUBKEYS: 'bdd76ce2934b2f591f9fad2ebe9da18f20d2921de527494ba00eeaa0a0efadcf'
# phpldapadmin:
# image: osixia/phpldapadmin:0.9.0
# ports:
@@ -128,3 +149,5 @@ volumes:
driver: local
redis-data:
driver: local
strfry-data:
driver: local

138
docker/strfry/strfry.conf Normal file
View File

@@ -0,0 +1,138 @@
##
## Default strfry config
##
# Directory that contains the strfry LMDB database (restart required)
db = "/var/lib/strfry/"
dbParams {
# Maximum number of threads/processes that can simultaneously have LMDB transactions open (restart required)
maxreaders = 256
# Size of mmap() to use when loading LMDB (default is 10TB, does *not* correspond to disk-space used) (restart required)
mapsize = 10995116277760
# Disables read-ahead when accessing the LMDB mapping. Reduces IO activity when DB size is larger than RAM. (restart required)
noReadAhead = false
}
events {
# Maximum size of normalised JSON, in bytes
maxEventSize = 65536
# Events newer than this will be rejected
rejectEventsNewerThanSeconds = 900
# Events older than this will be rejected
rejectEventsOlderThanSeconds = 94608000
# Ephemeral events older than this will be rejected
rejectEphemeralEventsOlderThanSeconds = 60
# Ephemeral events will be deleted from the DB when older than this
ephemeralEventsLifetimeSeconds = 300
# Maximum number of tags allowed
maxNumTags = 2000
# Maximum size for tag values, in bytes
maxTagValSize = 1024
}
relay {
# Interface to listen on. Use 0.0.0.0 to listen on all interfaces (restart required)
bind = "0.0.0.0"
# Port to open for the nostr websocket protocol (restart required)
port = 7777
# Set OS-limit on maximum number of open files/sockets (if 0, don't attempt to set) (restart required)
nofiles = 200000
# HTTP header that contains the client's real IP, before reverse proxying (ie x-real-ip) (MUST be all lower-case)
realIpHeader = ""
info {
# NIP-11: Name of this server. Short/descriptive (< 30 characters)
name = "Akkounts Nostr Relay"
# NIP-11: Detailed information about relay, free-form
description = "Local strfry instance for akkounts development"
# NIP-11: Administrative nostr pubkey, for contact purposes
pubkey = ""
# NIP-11: Alternative administrative contact (email, website, etc)
contact = ""
}
# Maximum accepted incoming websocket frame size (should be larger than max event) (restart required)
maxWebsocketPayloadSize = 131072
# Websocket-level PING message frequency (should be less than any reverse proxy idle timeouts) (restart required)
autoPingSeconds = 55
# If TCP keep-alive should be enabled (detect dropped connections to upstream reverse proxy)
enableTcpKeepalive = false
# How much uninterrupted CPU time a REQ query should get during its DB scan
queryTimesliceBudgetMicroseconds = 10000
# Maximum records that can be returned per filter
maxFilterLimit = 500
# Maximum number of subscriptions (concurrent REQs) a connection can have open at any time
maxSubsPerConnection = 20
writePolicy {
# If non-empty, path to an executable script that implements the writePolicy plugin logic
plugin = "/opt/strfry/strfry-policy.ts"
}
compression {
# Use permessage-deflate compression if supported by client. Reduces bandwidth, but slight increase in CPU (restart required)
enabled = true
# Maintain a sliding window buffer for each connection. Improves compression, but uses more memory (restart required)
slidingWindow = true
}
logging {
# Dump all incoming messages
dumpInAll = true
# Dump all incoming EVENT messages
dumpInEvents = false
# Dump all incoming REQ/CLOSE messages
dumpInReqs = false
# Log performance metrics for initial REQ database scans
dbScanPerf = true
# Log reason for invalid event rejection? Can be disabled to silence excessive logging
invalidEvents = true
}
numThreads {
# Ingester threads: route incoming requests, validate events/sigs (restart required)
ingester = 3
# reqWorker threads: Handle initial DB scan for events (restart required)
reqWorker = 3
# reqMonitor threads: Handle filtering of new events (restart required)
reqMonitor = 3
# negentropy threads: Handle negentropy protocol messages (restart required)
negentropy = 2
}
negentropy {
# Support negentropy protocol messages
enabled = true
# Maximum records that sync will process before returning an error
maxSyncEvents = 1000000
}
}

5
extras/strfry/deno.json Normal file
View File

@@ -0,0 +1,5 @@
{
"imports": {
"@nostr/tools": "jsr:@nostr/tools@^2.3.1"
}
}

196
extras/strfry/deno.lock generated Normal file
View 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"
]
}
}

View 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
View 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);
}

View File

@@ -0,0 +1,39 @@
import { load } from "https://deno.land/std@0.224.0/dotenv/mod.ts";
import { Client } from 'npm:ldapts';
const dirname = new URL('.', import.meta.url).pathname;
await load({ envPath: `${dirname}/.env`, export: true });
const opts = {
url: Deno.env.get("LDAP_URL"),
bindDN: Deno.env.get("LDAP_BIND_DN"),
password: Deno.env.get("LDAP_PASSWORD"),
searchDN: Deno.env.get("LDAP_SEARCH_DN"),
relayUrl: Deno.args[0]
}
const client = new Client({ url: opts.url });
try {
await client.bind(opts.bindDN, opts.password);
const { searchEntries } = await client.search(opts.searchDN, {
filter: `(nostrKey=*)`,
attributes: ['nostrKey']
});
const pubkeys = searchEntries.map(e => e.nostrKey);
const filter = JSON.stringify({ authors: pubkeys });
const p = Deno.run({ cmd: [
"strfry", "sync", opts.relayUrl,
"--dir", "down", "--filter", filter
]});
const result = await p.status();
Deno.exit(result.code);
} catch (ex) {
console.error(ex);
Deno.exit(1);
}

55
lib/nostr/event_kind.rb Normal file
View File

@@ -0,0 +1,55 @@
# frozen_string_literal: true
module Nostr
# Defines the event kinds that can be emitted by clients.
module EventKind
# The content is set to a stringified JSON object +{name: <username>, about: <string>,
# picture: <url, string>}+ describing the user who created the event. A relay may delete past set_metadata
# events once it gets a new one for the same pubkey.
#
# @return [Integer]
#
SET_METADATA = 0
# The content is set to the text content of a note (anything the user wants to say).
# Non-plaintext notes should instead use kind 1000-10000 as described in NIP-16.
#
# @return [Integer]
#
TEXT_NOTE = 1
# The content is set to the URL (e.g., wss://somerelay.com) of a relay the event creator wants to
# recommend to its followers.
#
# @return [Integer]
#
RECOMMEND_SERVER = 2
# A special event with kind 3, meaning "contact list" is defined as having a list of p tags, one for each of
# the followed/known profiles one is following.
#
# @return [Integer]
#
CONTACT_LIST = 3
# A special event with kind 4, meaning "encrypted direct message". An event of this kind has its +content+
# equal to the base64-encoded, aes-256-cbc encrypted string of anything a user wants to write, encrypted using a
# shared cipher generated by combining the recipient's public-key with the sender's private-key.
#
# @return [Integer]
#
ENCRYPTED_DIRECT_MESSAGE = 4
# NIP-57 Zap request
#
# @return [Integer]
#
ZAP_REQUEST = 9734
# NIP-57 Zap receipt
#
# @return [Integer]
#
ZAP_RECEIPT = 9735
end
end

14
lib/tasks/deno.rake Normal file
View File

@@ -0,0 +1,14 @@
namespace :deno do
desc "Download and prepare a Deno package for importmap"
task :prepare, [:package, :version] => :environment do |t, args|
unless args[:package] && args[:version]
raise "Usage: rake deno:prepare[package-name,version]"
end
# Build the package
system "deno run -A scripts/build_deno_package.ts #{args[:package]} #{args[:version]}"
# Pin the package using importmap
# system "bin/importmap pin #{args[:package]} --to vendor/javascript/#{args[:package]}/build.js"
end
end

View File

@@ -1,3 +1,6 @@
require "digest"
require "pp"
namespace :lndhub do
desc "Generate wallets for all users"
task :generate_wallets => :environment do |t, args|
@@ -22,6 +25,21 @@ namespace :lndhub do
puts "--\nSum of user balances: #{sum} sats"
end
desc "Create test invoice"
task :create_test_invoice, [:username, :amount, :description] => :environment do |t, args|
user = User.find_by cn: args[:username]
desc = args[:description]
hash = Digest::SHA256.hexdigest(desc)
invoice = LndhubManager::CreateUserInvoice.call(user: user, payload: {
"amount": args[:amount].to_i,
"description": desc,
"description_hash": hash
})
pp invoice
end
desc "Migrate existing accounts to lndhub.go"
task :migrate => :environment do |t, args|
# user = User.find_by cn: "jimmy"

View File

@@ -2,6 +2,7 @@
"name": "akkounts",
"private": true,
"dependencies": {
"@nostrify/nostrify": "npm:@jsr/nostrify__nostrify",
"@tailwindcss/forms": "^0.5.3",
"autoprefixer": "^10.4.13",
"postcss": "^8.4.19",
@@ -11,7 +12,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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Some files were not shown because too many files have changed in this diff Show More