resilience Exposure

This commit is contained in:
KoalaSat 2023-02-07 16:26:25 +01:00
parent f8b061f3f8
commit 887664b50d
No known key found for this signature in database
GPG Key ID: 2F7F61C6146AB157
17 changed files with 476 additions and 98 deletions

View File

@ -44,6 +44,12 @@ public class Event {
public void save(SQLiteDatabase database, String userPubKey, String relayUrl) {
if (isValid()) {
try {
ContentValues relayValues = new ContentValues();
relayValues.put("note_id", id);
relayValues.put("pubkey", pubkey);
relayValues.put("relay_url", relayUrl);
database.replace("nostros_notes_relays", null, relayValues);
if (kind.equals("0")) {
saveUserMeta(database);
} else if (kind.equals("1") || kind.equals("2")) {
@ -180,12 +186,6 @@ public class Event {
}
protected void saveNote(SQLiteDatabase database, String userPubKey, String relayUrl) {
ContentValues relayValues = new ContentValues();
relayValues.put("note_id", id);
relayValues.put("pubkey", pubkey);
relayValues.put("relay_url", relayUrl);
database.replace("nostros_notes_relays", null, relayValues);
ContentValues values = new ContentValues();
values.put("id", id);
values.put("content", content.replace("'", "''"));
@ -286,6 +286,7 @@ public class Event {
for (int i = 0; i < tags.length(); ++i) {
JSONArray tag = tags.getJSONArray(i);
String petId = tag.getString(1);
String relay = tag.getString(2);
String name = "";
if (tag.length() >= 4) {
name = tag.getString(3);
@ -298,6 +299,7 @@ public class Event {
values.put("name", name);
values.put("contact", true);
values.put("blocked", 0);
values.put("main_relay", relay);
values.put("pet_at", created_at);
database.insert("nostros_users", null, values);
}

View File

@ -40,8 +40,8 @@ public class DatabaseModule {
" picture TEXT,\n" +
" about TEXT,\n" +
" main_relay TEXT,\n" +
" contact BOOLEAN DEFAULT 0,\n" +
" follower BOOLEAN DEFAULT 0\n" +
" contact INT DEFAULT 0,\n" +
" follower INT DEFAULT 0\n" +
" );");
database.execSQL("CREATE TABLE IF NOT EXISTS nostros_relays(\n" +
" url TEXT PRIMARY KEY NOT NULL,\n" +
@ -56,11 +56,11 @@ public class DatabaseModule {
" sig TEXT NOT NULL,\n" +
" tags TEXT NOT NULL,\n" +
" conversation_id TEXT NOT NULL,\n" +
" read BOOLEAN DEFAULT 0\n" +
" read INT DEFAULT 0\n" +
" );");
try {
database.execSQL("ALTER TABLE nostros_notes ADD COLUMN user_mentioned BOOLEAN DEFAULT 0;");
database.execSQL("ALTER TABLE nostros_notes ADD COLUMN seen BOOLEAN DEFAULT 0;");
database.execSQL("ALTER TABLE nostros_notes ADD COLUMN user_mentioned INT DEFAULT 0;");
database.execSQL("ALTER TABLE nostros_notes ADD COLUMN seen INT DEFAULT 0;");
} catch (SQLException e) { }
try {
database.execSQL("ALTER TABLE nostros_users ADD COLUMN lnurl TEXT;");
@ -77,7 +77,7 @@ public class DatabaseModule {
" pubkey TEXT NOT NULL,\n" +
" sig TEXT NOT NULL,\n" +
" tags TEXT NOT NULL,\n" +
" positive BOOLEAN DEFAULT 1,\n" +
" positive INT DEFAULT 1,\n" +
" reacted_event_id TEXT,\n" +
" reacted_user_id TEXT\n" +
" );");
@ -98,15 +98,11 @@ public class DatabaseModule {
} catch (SQLException e) { }
try {
database.execSQL("ALTER TABLE nostros_users ADD COLUMN nip05 TEXT;");
} catch (SQLException e) { }
try {
database.execSQL("ALTER TABLE nostros_users ADD COLUMN valid_nip05 BOOLEAN DEFAULT 0;");
database.execSQL("ALTER TABLE nostros_users ADD COLUMN valid_nip05 INT DEFAULT 0;");
} catch (SQLException e) { }
try {
database.execSQL("ALTER TABLE nostros_notes ADD COLUMN repost_id TEXT;");
} catch (SQLException e) { }
try {
database.execSQL("ALTER TABLE nostros_relays ADD COLUMN active BOOLEAN DEFAULT 1;");
database.execSQL("ALTER TABLE nostros_relays ADD COLUMN active INT DEFAULT 1;");
} catch (SQLException e) { }
try {
database.execSQL("CREATE INDEX nostros_notes_repost_id_created_at_index ON nostros_notes(repost_id, pubkey, created_at); ");
@ -122,12 +118,9 @@ public class DatabaseModule {
database.execSQL("CREATE INDEX nostros_users_contact_follower_index ON nostros_users(contact, follower); ");
database.execSQL("CREATE INDEX nostros_users_id_name_index ON nostros_users(id, name); ");
} catch (SQLException e) { }
try {
database.execSQL("DROP TABLE IF EXISTS nostros_config;");
} catch (SQLException e) { }
try {
database.execSQL("ALTER TABLE nostros_users ADD COLUMN blocked BOOLEAN DEFAULT 0;");
database.execSQL("ALTER TABLE nostros_users ADD COLUMN blocked INT DEFAULT 0;");
} catch (SQLException e) { }
try {
database.execSQL("CREATE TABLE IF NOT EXISTS nostros_notes_relays(\n" +
@ -140,9 +133,13 @@ public class DatabaseModule {
database.execSQL("CREATE INDEX nostros_notes_relays_pubkey_index ON nostros_notes_relays(pubkey);");
} catch (SQLException e) { }
try {
database.execSQL("ALTER TABLE nostros_relays ADD COLUMN global_feed BOOLEAN DEFAULT 1;");
database.execSQL("ALTER TABLE nostros_users ADD COLUMN pet_at INT;");
database.execSQL("ALTER TABLE nostros_users ADD COLUMN follower_at INT;");
database.execSQL("ALTER TABLE nostros_relays ADD COLUMN global_feed INT DEFAULT 1;");
} catch (SQLException e) { }
try {
database.execSQL("ALTER TABLE nostros_relays ADD COLUMN resilient INT DEFAULT 0;");
database.execSQL("ALTER TABLE nostros_relays ADD COLUMN manual INT DEFAULT 1;");
} catch (SQLException e) { }
}

View File

@ -1,45 +1 @@
export const defaultRelays = [
'wss://brb.io',
'wss://damus.io',
'wss://nostr-pub.wellorder.net',
'wss://nostr.swiss-enigma.ch',
'wss://nostr.onsats.org',
'wss://nostr-pub.semisol.dev',
'wss://nostr.openchain.fr',
'wss://relay.nostr.info',
'wss://nostr.oxtr.dev',
'wss://nostr.ono.re',
'wss://relay.grunch.dev',
'wss://nostr.developer.li',
]
export const REGEX_SOCKET_LINK = /^wss:\/\/.*\..*$/i
export const relayColors = [
'#3016dd',
'#43e1ef',
'#ef3b50',
'#d3690c',
'#43e23b',
'#3a729e',
'#805de2',
'#b2ff5b',
'#eaa123',
'#ba7a3b',
'#90c900',
'#26e08c',
'#090660',
'#9edb62',
'#db48f2',
'#5d14aa',
'#f2d859',
'#0b8458',
'#cdea10',
'#6473e0',
'#6721a5',
'#f76f8c',
'#ce2780',
'#403ba0',
'#a9f41d',
'#BBF067',
]
export const REGEX_SOCKET_LINK = /^(ws|wss):\/\/.*\..*$/i

View File

@ -3,7 +3,7 @@ import RelayPool from '../lib/nostr/RelayPool/intex'
import { AppContext } from './AppContext'
import { DeviceEventEmitter } from 'react-native'
import debounce from 'lodash.debounce'
import { createRelay, getRelays, Relay } from '../Functions/DatabaseFunctions/Relays'
import { getRelays, Relay } from '../Functions/DatabaseFunctions/Relays'
import { UserContext } from './UserContext'
export interface RelayPoolContextProps {
@ -74,6 +74,7 @@ export const RelayPoolContextProvider = ({
DeviceEventEmitter.addListener('WebsocketEvent', debouncedEventIdHandler)
DeviceEventEmitter.addListener('WebsocketConfirmation', debouncedConfirmationHandler)
const initRelayPool = new RelayPool(privateKey)
await initRelayPool.resilientMode(database, publicKey)
initRelayPool.connect(publicKey, () => setRelayPoolReady(true))
setRelayPool(initRelayPool)
loadRelays()
@ -140,18 +141,6 @@ export const RelayPoolContextProvider = ({
}
}, [publicKey])
useEffect(() => {
if (database) {
getRelays(database).then((results) => {
if (results.length === 0) {
createRelay(database, 'wss://brb.io')
createRelay(database, 'wss://damus.io')
createRelay(database, 'wss://nostr-pub.wellorder.net')
}
})
}
}, [database])
return (
<RelayPoolContext.Provider
value={{

View File

@ -0,0 +1,50 @@
import { QuickSQLiteConnection } from 'react-native-quick-sqlite'
export interface NoteRelay {
relay_url: string
pubkey: string
note_id: number
}
export const getNoteRelaysUsage: (
db: QuickSQLiteConnection,
) => Promise<Record<string, number>> = async (db) => {
const result: Record<string, number> = {}
const query = `
SELECT relay_url, COUNT(*) as count FROM nostros_notes_relays
LEFT JOIN
nostros_users ON nostros_users.id = nostros_notes_relays.pubkey
WHERE nostros_users.contact > 0
GROUP BY relay_url
`
const resultSet = db.execute(query)
if (resultSet?.rows?.length) {
for (let index = 0; index < resultSet?.rows?.length; index++) {
const row = resultSet?.rows?.item(index)
result[row.relay_url] = row.count
}
}
return result
}
export const getNoteRelaysPresence: (
db: QuickSQLiteConnection,
) => Promise<Record<string, string[]>> = async (db) => {
const result: Record<string, string[]> = {}
const query = `
SELECT relay_url, pubkey FROM nostros_notes_relays
LEFT JOIN
nostros_users ON nostros_users.id = nostros_notes_relays.pubkey
WHERE nostros_users.contact > 0
GROUP BY relay_url, pubkey
ORDER BY random()
`
const resultSet = db.execute(query)
if (resultSet?.rows?.length) {
for (let index = 0; index < resultSet?.rows?.length; index++) {
const row = resultSet?.rows?.item(index)
result[row.relay_url] = [...(result[row.relay_url] ?? []), row.pubkey]
}
}
return result
}

View File

@ -1,11 +1,15 @@
import { QueryResult, QuickSQLiteConnection } from 'react-native-quick-sqlite'
import { getItems } from '..'
import { median } from '../../NativeFunctions'
import { getNoteRelaysUsage } from '../NotesRelays'
export interface Relay {
url: string
name?: string
active?: number
global_feed?: number
resilient?: number
manual?: number
}
export interface RelayInfo {
@ -61,3 +65,39 @@ export const createRelay: (db: QuickSQLiteConnection, url: string) => Promise<Qu
`
return db.execute(query, [url])
}
export const createResilientRelay: (
db: QuickSQLiteConnection,
url: string,
) => Promise<QueryResult> = async (db, url) => {
const query = `
INSERT OR IGNORE INTO nostros_relays (url, resilient, active) VALUES (?, ?, ?)
`
return db.execute(query, [url, 1, 0])
}
export const activateResilientRelays: (
db: QuickSQLiteConnection,
relayUrls: string[],
) => Promise<QueryResult> = async (db, relayUrls) => {
const userQuery = `UPDATE nostros_relays SET resilient = 1 WHERE url IN ('${relayUrls.join(
"', '",
)}');`
return db.execute(userQuery)
}
export const desactivateResilientRelays: (
db: QuickSQLiteConnection,
) => Promise<QueryResult> = async (db) => {
const userQuery = `UPDATE nostros_relays SET resilient = 0 WHERE resilient = 1;`
return db.execute(userQuery)
}
export const getResilientRelays: (db: QuickSQLiteConnection) => Promise<string[]> = async (db) => {
const relayUsage = await getNoteRelaysUsage(db)
const medianUsage = median(Object.values(relayUsage))
const resilientRelays = Object.keys(relayUsage).sort((n1: string, n2: string) => {
return Math.abs(relayUsage[n1] - medianUsage) - Math.abs(relayUsage[n2] - medianUsage)
})
return resilientRelays
}

View File

@ -68,6 +68,25 @@ export const addUser: (pubKey: string, db: QuickSQLiteConnection) => Promise<Que
return db.execute(query, [pubKey])
}
export const getMainRelays: (
db: QuickSQLiteConnection,
) => Promise<Record<string, { count: number }>> = async (db) => {
const result: Record<string, { count: number }> = {}
const query = `
SELECT main_relay, COUNT(*) as count FROM nostros_users
WHERE nostros_users.contact > 0
GROUP BY main_relay
`
const resultSet = db.execute(query)
if (resultSet?.rows?.length) {
for (let index = 0; index < resultSet?.rows?.length; index++) {
const row = resultSet?.rows?.item(index)
console.log(row)
}
}
return result
}
export const getContactsCount: (db: QuickSQLiteConnection) => Promise<number> = async (db) => {
const countQuery = 'SELECT COUNT(*) FROM nostros_users WHERE contact = 1'
const resultSet = db.execute(countQuery)

View File

@ -32,6 +32,8 @@ export const dropTables: (db: QuickSQLiteConnection) => Promise<BatchQueryResult
const dropQueries: Array<[string, [any[] | any[][]]]> = [
['DELETE FROM nostros_users;', [[]]],
['DELETE FROM nostros_notes;', [[]]],
['DELETE FROM nostros_reactions;', [[]]],
['DELETE FROM nostros_notes_relays;', [[]]],
['DELETE FROM nostros_direct_messages;', [[]]],
]
return db.executeBatch(dropQueries)

View File

@ -1,5 +1,3 @@
import { relayColors } from '../../Constants/Relay'
export const handleInfinityScroll: (event: any) => boolean = (event) => {
const mHeight = event.nativeEvent.layoutMeasurement.height
const cSize = event.nativeEvent.contentSize.height
@ -9,6 +7,35 @@ export const handleInfinityScroll: (event: any) => boolean = (event) => {
return false
}
export const relayColors = [
'#3016dd',
'#43e1ef',
'#ef3b50',
'#d3690c',
'#43e23b',
'#3a729e',
'#805de2',
'#b2ff5b',
'#eaa123',
'#ba7a3b',
'#90c900',
'#26e08c',
'#090660',
'#9edb62',
'#db48f2',
'#5d14aa',
'#f2d859',
'#0b8458',
'#cdea10',
'#6473e0',
'#6721a5',
'#f76f8c',
'#ce2780',
'#403ba0',
'#a9f41d',
'#BBF067',
]
export const relayToColor: (string: string) => string = (string) => {
let hash = 0
for (let i = 0; i < string.length; i++) {
@ -61,3 +88,9 @@ export const validNip21: (string: string | undefined) => boolean = (string) => {
export const randomInt: (min: number, max: number) => number = (min, max) =>
Math.floor(Math.random() * (max - min + 1)) + min
export const median: (arr: number[]) => number = (arr) => {
const mid = Math.floor(arr.length / 2)
const nums = [...arr].sort((a, b) => a - b)
return arr.length % 2 !== 0 ? nums[mid] : (nums[mid - 1] + nums[mid]) / 2
}

View File

@ -183,6 +183,14 @@
}
},
"relaysPage": {
"resilienceTitle": "Resilience (experimental)",
"resilienceDescription": "The nostr protocol provides a good amount of tools to build a desentralized network, but unlike other desentralized protocols such as Bitcoin, it's not inherent by defaul on his usage.\n\nTo ahcieve such goal, clients and relays should activally colaborate. Nostros resiliense mode tries to, first, expose these concerns to their users, and secondly, start experimening with new pattern and way to help on network' resilency.\n\nFunctional implementation will soon endup appearing on Nostros by default.",
"resilienceCategories": "Resilience exposure",
"resilienceCategoriesDescription": "A first approach Nostros is testing is randomly calculate (based on received contats' events) what are the most resilient relays. It tries to avoid both relays with too much events (centralization) and relays with almost no events (battery draining).\n\nThe goal is to generate a list of 5 relays and cover all contacts. But if this is not possible, it will search among other realys, warning to the user about it.",
"centralized": "Centralized",
"small": "Small",
"contacts": "# Contacts",
"resilienceMode": "Resilience (experimental)",
"relayName": "Addresse",
"globalFeed": "Globaler Feed",
"active": "Aktiv",

View File

@ -166,6 +166,14 @@
"newMessages": "{{newNotesCount}} new notes. Pull to refresh."
},
"relaysPage": {
"resilienceTitle": "Resilience (experimental)",
"resilienceDescription": "The nostr protocol provides a good amount of tools to build a desentralized network, but unlike other desentralized protocols such as Bitcoin, it's not inherent by defaul on his usage.\n\nTo ahcieve such goal, clients and relays should activally colaborate. Nostros resiliense mode tries to, first, expose these concerns to their users, and secondly, start experimening with new pattern and way to help on network' resilency.\n\nFunctional implementation will soon endup appearing on Nostros by default.",
"resilienceCategories": "Resilience exposure",
"resilienceCategoriesDescription": "A first approach Nostros is testing is randomly calculate (based on received contats' events) what are the most resilient relays. It tries to avoid both relays with too much events (centralization) and relays with almost no events (battery draining).\n\nThe goal is to generate a list of 5 relays and cover all contacts. But if this is not possible, it will search among other realys, warning to the user about it.",
"centralized": "Centralized",
"small": "Small",
"contacts": "# Contacts",
"resilienceMode": "Resilience (experimental)",
"relayName": "Address",
"globalFeed": "Global feed",
"active": "Active",

View File

@ -179,6 +179,14 @@
}
},
"relaysPage": {
"resilienceTitle": "Resilience (experimental)",
"resilienceDescription": "The nostr protocol provides a good amount of tools to build a desentralized network, but unlike other desentralized protocols such as Bitcoin, it's not inherent by defaul on his usage.\n\nTo ahcieve such goal, clients and relays should activally colaborate. Nostros resiliense mode tries to, first, expose these concerns to their users, and secondly, start experimening with new pattern and way to help on network' resilency.\n\nFunctional implementation will soon endup appearing on Nostros by default.",
"resilienceCategories": "Resilience exposure",
"resilienceCategoriesDescription": "A first approach Nostros is testing is randomly calculate (based on received contats' events) what are the most resilient relays. It tries to avoid both relays with too much events (centralization) and relays with almost no events (battery draining).\n\nThe goal is to generate a list of 5 relays and cover all contacts. But if this is not possible, it will search among other realys, warning to the user about it.",
"centralized": "Centralizado",
"small": "Pequeño",
"contacts": "Nº contactos",
"resilienceMode": "Resiliencia (experimental)",
"relayName": "Dirección",
"globalFeed": "Feed global",
"active": "Activo",

View File

@ -178,6 +178,14 @@
}
},
"relaysPage": {
"resilienceTitle": "Resilience (experimental)",
"resilienceDescription": "The nostr protocol provides a good amount of tools to build a desentralized network, but unlike other desentralized protocols such as Bitcoin, it's not inherent by defaul on his usage.\n\nTo ahcieve such goal, clients and relays should activally colaborate. Nostros resiliense mode tries to, first, expose these concerns to their users, and secondly, start experimening with new pattern and way to help on network' resilency.\n\nFunctional implementation will soon endup appearing on Nostros by default.",
"resilienceCategories": "Resilience exposure",
"resilienceCategoriesDescription": "A first approach Nostros is testing is randomly calculate (based on received contats' events) what are the most resilient relays. It tries to avoid both relays with too much events (centralization) and relays with almost no events (battery draining).\n\nThe goal is to generate a list of 5 relays and cover all contacts. But if this is not possible, it will search among other realys, warning to the user about it.",
"centralized": "Centralized",
"small": "Small",
"contacts": "# Contacts",
"resilienceMode": "Resilience (experimental)",
"relayName": "Adresse",
"globalFeed": "Flux global",
"active": "Actif",

View File

@ -178,6 +178,14 @@
}
},
"relaysPage": {
"resilienceTitle": "Resilience (experimental)",
"resilienceDescription": "The nostr protocol provides a good amount of tools to build a desentralized network, but unlike other desentralized protocols such as Bitcoin, it's not inherent by defaul on his usage.\n\nTo ahcieve such goal, clients and relays should activally colaborate. Nostros resiliense mode tries to, first, expose these concerns to their users, and secondly, start experimening with new pattern and way to help on network' resilency.\n\nFunctional implementation will soon endup appearing on Nostros by default.",
"resilienceCategories": "Resilience exposure",
"resilienceCategoriesDescription": "A first approach Nostros is testing is randomly calculate (based on received contats' events) what are the most resilient relays. It tries to avoid both relays with too much events (centralization) and relays with almost no events (battery draining).\n\nThe goal is to generate a list of 5 relays and cover all contacts. But if this is not possible, it will search among other realys, warning to the user about it.",
"centralized": "Centralized",
"small": "Small",
"contacts": "# Contacts",
"resilienceMode": "Resilience (experimental)",
"relayName": "Address",
"globalFeed": "Общая лента",
"active": "Active",

View File

@ -36,7 +36,7 @@ interface ConversationPageProps {
export const ConversationPage: React.FC<ConversationPageProps> = ({ route }) => {
const theme = useTheme()
const scrollViewRef = useRef<ScrollView>()
const { database } = useContext(AppContext)
const { database, setRefreshBottomBarAt } = useContext(AppContext)
const { relayPool, lastEventId } = useContext(RelayPoolContext)
const { publicKey, privateKey, name } = useContext(UserContext)
const otherPubKey = useMemo(() => route.params.pubKey, [])
@ -64,6 +64,7 @@ export const ConversationPage: React.FC<ConversationPageProps> = ({ route }) =>
if (database && publicKey) {
const conversationId = route.params?.conversationId
updateConversationRead(conversationId, database)
setRefreshBottomBarAt(getUnixTime(new Date()))
getUser(otherPubKey, database).then((user) => {
if (user) setOtherUser(user)
})

View File

@ -4,7 +4,7 @@ import Clipboard from '@react-native-clipboard/clipboard'
import { useTranslation } from 'react-i18next'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { Relay } from '../../Functions/DatabaseFunctions/Relays'
import { defaultRelays, REGEX_SOCKET_LINK } from '../../Constants/Relay'
import { REGEX_SOCKET_LINK } from '../../Constants/Relay'
import {
List,
Switch,
@ -22,6 +22,21 @@ import { relayToColor } from '../../Functions/NativeFunctions'
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
import { useFocusEffect } from '@react-navigation/native'
export const defaultRelays = [
'wss://brb.io',
'wss://damus.io',
'wss://nostr-pub.wellorder.net',
'wss://nostr.swiss-enigma.ch',
'wss://nostr.onsats.org',
'wss://nostr-pub.semisol.dev',
'wss://nostr.openchain.fr',
'wss://relay.nostr.info',
'wss://nostr.oxtr.dev',
'wss://nostr.ono.re',
'wss://relay.grunch.dev',
'wss://nostr.developer.li',
]
export const RelaysPage: React.FC = () => {
const defaultRelayInput = React.useMemo(() => 'wss://', [])
const { updateRelayItem, addRelayItem, removeRelayItem, relays, relayPool } =
@ -30,6 +45,7 @@ export const RelaysPage: React.FC = () => {
const theme = useTheme()
const bottomSheetAddRef = React.useRef<RBSheet>(null)
const bottomSheetEditRef = React.useRef<RBSheet>(null)
const bottomSheetResilenseRef = React.useRef<RBSheet>(null)
const [selectedRelay, setSelectedRelay] = useState<Relay>()
const [addRelayInput, setAddRelayInput] = useState<string>(defaultRelayInput)
const [showNotification, setShowNotification] = useState<string>()
@ -119,14 +135,14 @@ export const RelaysPage: React.FC = () => {
}
}, [])
const myRelays = relays
.filter((relay) => !defaultRelays.includes(relay.url))
.sort((a, b) => {
const relayList = relays.sort((a, b) => {
if (a.url > b.url) return 1
if (a.url < b.url) return -1
return 0
})
const myRelays = relayList.filter((relay) => !defaultRelays.includes(relay.url))
const renderItem: ListRenderItem<Relay> = ({ item, index }) => {
return (
<List.Item
@ -162,8 +178,94 @@ export const RelaysPage: React.FC = () => {
)
}
const renderResilienceItem: (item: string, index: number, type?: string) => JSX.Element = (
item,
index,
type,
) => {
return (
<List.Item
key={index}
title={item.split('wss://')[1]?.split('/')[0]}
left={() => (
<MaterialCommunityIcons
style={styles.relayColor}
name='circle'
color={relayToColor(item)}
/>
)}
right={() => (
<>
{type === 'centralized' && (
<Text style={[styles.smallRelay, { color: theme.colors.errorContainer }]}>
{relayPool?.resilientAssignation.centralizedRelays[item] &&
t('relaysPage.centralized')}
</Text>
)}
{type === 'small' && (
<Text style={[styles.smallRelay, { color: theme.colors.error }]}>
{relayPool?.resilientAssignation.smallRelays[item] && t('relaysPage.small')}
</Text>
)}
<Text>
{type === undefined && relayPool?.resilientAssignation.resilientRelays[item]?.length}
{type === 'small' && relayPool?.resilientAssignation.smallRelays[item]?.length}
{type === 'centralized' &&
relayPool?.resilientAssignation.centralizedRelays[item]?.length}
</Text>
</>
)}
/>
)
}
return (
<View style={styles.container}>
<ScrollView horizontal={false}>
<View style={styles.titleWrapper}>
<View style={styles.title}>
<Text style={styles.title} variant='titleMedium'>
{t('relaysPage.resilienceMode')}
</Text>
<IconButton
style={styles.titleAction}
icon='question'
size={20}
onPress={() => bottomSheetResilenseRef.current?.open()}
/>
</View>
<Divider />
</View>
<List.Item
title={t('relaysPage.relayName')}
right={() => <Text style={styles.listHeader}>{t('relaysPage.contacts')}</Text>}
/>
<FlatList
showsVerticalScrollIndicator={false}
data={Object.keys(relayPool?.resilientAssignation.resilientRelays ?? {})}
renderItem={(data) => renderResilienceItem(data.item, data.index)}
/>
<View style={styles.titleWrapper}>
<Divider />
</View>
<FlatList
showsVerticalScrollIndicator={false}
data={Object.keys(relayPool?.resilientAssignation.centralizedRelays ?? {})}
renderItem={(data) => renderResilienceItem(data.item, data.index, 'centralized')}
/>
<FlatList
showsVerticalScrollIndicator={false}
data={Object.keys(relayPool?.resilientAssignation.smallRelays ?? {})}
renderItem={(data) => renderResilienceItem(data.item, data.index, 'small')}
/>
{myRelays.length > 0 && (
<>
<View style={styles.titleWrapper}>
<Text style={styles.title} variant='titleMedium'>
{t('relaysPage.myList')}
</Text>
<Divider />
</View>
<List.Item
title={t('relaysPage.relayName')}
right={() => (
@ -173,15 +275,6 @@ export const RelaysPage: React.FC = () => {
</>
)}
/>
<ScrollView horizontal={false}>
{myRelays.length > 0 && (
<>
<View style={styles.titleWrapper}>
<Text style={styles.title} variant='titleMedium'>
{t('relaysPage.myList')}
</Text>
<Divider />
</View>
<FlatList
showsVerticalScrollIndicator={false}
data={myRelays}
@ -195,6 +288,15 @@ export const RelaysPage: React.FC = () => {
</Text>
<Divider />
</View>
<List.Item
title={t('relaysPage.relayName')}
right={() => (
<>
<Text style={styles.listHeader}>{t('relaysPage.globalFeed')}</Text>
<Text style={styles.listHeader}>{t('relaysPage.active')}</Text>
</>
)}
/>
<FlatList
showsVerticalScrollIndicator={false}
data={defaultRelays.map(
@ -227,6 +329,18 @@ export const RelaysPage: React.FC = () => {
{t(`relaysPage.notifications.${showNotification}`)}
</Snackbar>
)}
<RBSheet
ref={bottomSheetResilenseRef}
closeOnDragDown={true}
customStyles={rbSheetCustomStyles}
>
<View style={styles.resilienceDrawer}>
<Text variant='headlineSmall'>{t('relaysPage.resilienceTitle')}</Text>
<Text variant='bodyMedium'>{t('relaysPage.resilienceDescription')}</Text>
<Text variant='titleMedium'>{t('relaysPage.resilienceCategories')}</Text>
<Text variant='bodyMedium'>{t('relaysPage.resilienceCategoriesDescription')}</Text>
</View>
</RBSheet>
<RBSheet ref={bottomSheetAddRef} closeOnDragDown={true} customStyles={rbSheetCustomStyles}>
<View style={styles.addRelay}>
<View style={styles.bottomDrawerButton}>
@ -294,6 +408,12 @@ const styles = StyleSheet.create({
},
title: {
marginBottom: 8,
flexDirection: 'row',
justifyContent: 'space-between',
},
titleAction: {
marginTop: -5,
marginBottom: -10,
},
bottomDrawerButton: {
paddingBottom: 16,
@ -341,6 +461,16 @@ const styles = StyleSheet.create({
marginBottom: 26,
marginTop: 26,
},
centralizedRelay: {
paddingRight: 10,
},
smallRelay: {
paddingRight: 10,
},
resilienceDrawer: {
height: 600,
justifyContent: 'space-between',
},
})
export default RelaysPage

View File

@ -1,6 +1,14 @@
// import { spawnThread } from 'react-native-multithreading'
import { signEvent, validateEvent, Event } from '../Events'
import RelayPoolModule from '../../Native/WebsocketModule'
import { QuickSQLiteConnection } from 'react-native-quick-sqlite'
import { median, randomInt } from '../../../Functions/NativeFunctions'
import {
activateResilientRelays,
createResilientRelay,
desactivateResilientRelays,
} from '../../../Functions/DatabaseFunctions/Relays'
import { getNoteRelaysPresence } from '../../../Functions/DatabaseFunctions/NotesRelays'
export interface RelayFilters {
ids?: string[]
@ -17,14 +25,42 @@ export interface RelayMessage {
data: string
}
export interface ResilientAssignation {
resilientRelays: Record<string, string[]>
smallRelays: Record<string, string[]>
centralizedRelays: Record<string, string[]>
}
export const fallbackRelays = [
'wss://brb.io',
'wss://damus.io',
'wss://nostr-pub.wellorder.net',
'wss://nostr.swiss-enigma.ch',
'wss://nostr.onsats.org',
'wss://nostr-pub.semisol.dev',
'wss://nostr.openchain.fr',
'wss://relay.nostr.info',
'wss://nostr.oxtr.dev',
'wss://nostr.ono.re',
'wss://relay.grunch.dev',
'wss://nostr.developer.li',
]
class RelayPool {
constructor(privateKey?: string) {
this.privateKey = privateKey
this.subscriptions = {}
this.resilientAssignation = {
resilientRelays: {},
smallRelays: {},
centralizedRelays: {},
fallback: {},
}
}
private readonly privateKey?: string
private subscriptions: Record<string, string[]>
public resilientAssignation: ResilientAssignation
private readonly send: (message: object, globalFeed?: boolean) => void = async (
message,
@ -39,6 +75,89 @@ class RelayPool {
RelayPoolModule.connect(publicKey, onEventId)
}
public readonly resilientMode: (db: QuickSQLiteConnection, publicKey: string) => void = async (
db,
) => {
await desactivateResilientRelays(db)
// Get relays with contacts' pubkeys with at least one event found, randomly sorted
const relaysPresence: Record<string, string[]> = await getNoteRelaysPresence(db)
// Median of users per relay
const medianUsage = median(
Object.keys(relaysPresence).map((relay) => relaysPresence[relay].length),
)
// Sort relays by abs distance from the mediam
const relaysByPresence = Object.keys(relaysPresence).sort((n1: string, n2: string) => {
return (
Math.abs(relaysPresence[n1].length - medianUsage) -
Math.abs(relaysPresence[n2].length - medianUsage)
)
})
// Get top5 relays closer to the mediam
const medianRelays = relaysByPresence.slice(0, 5)
// Set helpers
let biggestRelayLenght = 0
this.resilientAssignation.resilientRelays = {}
const allocatedUsers: string[] = []
medianRelays.forEach((relayUrl) => {
this.resilientAssignation.resilientRelays[relayUrl] = []
const length = relaysPresence[relayUrl].length
if (length > biggestRelayLenght) biggestRelayLenght = length
})
// Iterate over the N index of top5 relay list removing identical pubkey from others
for (let index = 0; index < biggestRelayLenght - 1; index++) {
medianRelays.forEach((relayUrl) => {
const pubKey = relaysPresence[relayUrl][index]
if (pubKey && !allocatedUsers.includes(pubKey)) {
allocatedUsers.push(pubKey)
this.resilientAssignation.resilientRelays[relayUrl].push(pubKey)
}
})
}
// Iterate over remaining relays and assigns as much remaining users as possible
relaysByPresence.slice(5, relaysByPresence.length).forEach((relayUrl) => {
relaysPresence[relayUrl].forEach((pubKey) => {
if (!allocatedUsers.includes(pubKey)) {
allocatedUsers.push(pubKey)
if (relaysPresence[relayUrl].length > medianUsage) {
if (!this.resilientAssignation.centralizedRelays[relayUrl])
this.resilientAssignation.centralizedRelays[relayUrl] = []
this.resilientAssignation.centralizedRelays[relayUrl].push(pubKey)
} else {
if (!this.resilientAssignation.smallRelays[relayUrl])
this.resilientAssignation.smallRelays[relayUrl] = []
this.resilientAssignation.smallRelays[relayUrl].push(pubKey)
}
}
})
})
// Target list size is 5, adds random relays from a fallback list
const resilientUrls = [
...Object.keys(this.resilientAssignation.resilientRelays),
...Object.keys(this.resilientAssignation.centralizedRelays),
...Object.keys(this.resilientAssignation.smallRelays),
]
while (resilientUrls.length < 5) {
let fallbackRelay = ''
while (fallbackRelay === '') {
const randomRelayIndex = randomInt(0, fallbackRelays.length - 1)
if (!resilientUrls.includes(fallbackRelays[randomRelayIndex])) {
fallbackRelay = fallbackRelays[randomRelayIndex]
}
}
resilientUrls.push(fallbackRelay)
this.resilientAssignation.centralizedRelays[fallbackRelay] = []
}
// Stores in DB
resilientUrls.forEach(async (relayUrl) => await createResilientRelay(db, relayUrl))
activateResilientRelays(db, resilientUrls)
}
public readonly add: (relayUrl: string, callback?: () => void) => void = async (
relayUrl,
callback = () => {},