3 Commits

7 changed files with 145 additions and 179 deletions

24
LICENSE Normal file
View File

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>

View File

@ -33,6 +33,8 @@ func main() {
relay.Info.PubKey = getEnv("RELAY_PUBKEY", "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798")
relay.Info.Description = getEnv("RELAY_DESCRIPTION", "LUMINA Relay")
relay.Info.Icon = getEnv("RELAY_ICON", "https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fliquipedia.net%2Fcommons%2Fimages%2F3%2F35%2FSCProbe.jpg&f=1&nofb=1&ipt=0cbbfef25bce41da63d910e86c3c343e6c3b9d63194ca9755351bb7c2efa3359&ipo=images")
relay.Info.Software = "lumina-relay"
relay.Info.Version = "0.1.0"
// Print relay information
fmt.Printf("Name: %s\n", relay.Info.Name)
@ -46,6 +48,10 @@ func main() {
panic(err)
}
// Initialize trending system to start background calculations
fmt.Println("Initializing trending system...")
trending.Initialize(db.DB.DB)
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
@ -252,7 +258,7 @@ func main() {
mux.HandleFunc("/api/trending/kind20", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
trendingPosts, err := trending.GetTrendingScoreKind20(db.DB.DB)
trendingPosts, err := trending.GetTrendingKind20(db.DB.DB)
if err != nil {
http.Error(w, fmt.Sprintf("Error getting trending posts: %v", err), http.StatusInternalServerError)
return

View File

@ -1,15 +0,0 @@
## Basic
A trending algo over simplified based on reactions
## Score
Add a trending score calculation that considers time decay and multiple engagement types.
Key improvements:
Considers multiple engagement types with different weights
Implements time decay using a power law function
Adds a base time offset (7200 seconds = 2 hours) to smooth out early scoring
More comprehensive trending score instead of just raw counts
The time decay formula is similar to HackerNews' algorithm but adjusted for shorter-term trending content. You might want to tune the weights and decay factors based on your specific needs.

View File

@ -1,71 +0,0 @@
package trending
import (
"database/sql"
"encoding/json"
"time"
"git.highperfocused.tech/highperfocused/lumina-relay/relay/cache"
)
var (
trendingCache = cache.New()
cacheDuration = 5 * time.Minute
)
// GetTrendingBasicKind20 returns the top 20 trending posts of kind 20 from the last 24 hours
func GetTrendingBasicKind20(db *sql.DB) ([]Post, error) {
if cached, ok := trendingCache.Get("trending_kind_20"); ok {
return cached.([]Post), nil
}
query := `
WITH reactions AS (
SELECT
tags_expanded.value->1 #>> '{}' AS original_event_id,
COUNT(*) as reaction_count
FROM event e
CROSS JOIN LATERAL jsonb_array_elements(tags) as tags_expanded(value)
WHERE e.kind::text = '7'
AND e.created_at >= extract(epoch from now() - interval '24 hours')::bigint
AND tags_expanded.value->0 #>> '{}' = 'e'
GROUP BY tags_expanded.value->1 #>> '{}'
)
SELECT
e.id,
e.pubkey,
to_timestamp(e.created_at) as created_at,
e.kind,
e.content,
e.tags,
COALESCE(r.reaction_count, 0) as reaction_count
FROM event e
LEFT JOIN reactions r ON e.id = r.original_event_id
WHERE e.kind::text = '20'
AND e.created_at >= extract(epoch from now() - interval '24 hours')::bigint
ORDER BY reaction_count DESC, e.created_at DESC
LIMIT 20
`
rows, err := db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var trendingPosts []Post
for rows.Next() {
var post Post
var tagsJSON []byte
if err := rows.Scan(&post.ID, &post.PubKey, &post.CreatedAt, &post.Kind, &post.Content, &tagsJSON, &post.ReactionCount); err != nil {
return nil, err
}
if err := json.Unmarshal(tagsJSON, &post.Tags); err != nil {
return nil, err
}
trendingPosts = append(trendingPosts, post)
}
trendingCache.Set("trending_kind_20", trendingPosts, cacheDuration)
return trendingPosts, nil
}

114
relay/trending/kinds.go Normal file
View File

@ -0,0 +1,114 @@
package trending
import (
"database/sql"
"encoding/json"
"fmt"
"time"
"git.highperfocused.tech/highperfocused/lumina-relay/relay/cache"
)
type Post struct {
ID string `json:"id"`
PubKey string `json:"pubkey"`
CreatedAt time.Time `json:"created_at"`
Kind string `json:"kind"`
Content string `json:"content"`
Tags [][]string `json:"tags"`
ReactionCount int `json:"reaction_count"`
}
var (
trendingCache = cache.New()
cacheDuration = 35 * time.Minute // Slightly longer than update interval
updateInterval = 30 * time.Minute
)
// Initialize sets up the background updating of trending calculations
// This should be called once at application startup
func Initialize(db *sql.DB) {
// Perform initial calculation
if _, err := calculateTrendingKind20(db); err != nil {
fmt.Printf("Error in initial trending calculation: %v\n", err)
}
// Set up periodic updates
go func() {
ticker := time.NewTicker(updateInterval)
defer ticker.Stop()
for range ticker.C {
if _, err := calculateTrendingKind20(db); err != nil {
fmt.Printf("Error updating trending data: %v\n", err)
} else {
fmt.Printf("Successfully updated trending data at %s\n", time.Now().Format(time.RFC3339))
}
}
}()
}
// GetTrendingKind20 returns the top 20 trending posts of kind 20 from the last 24 hours
// It returns cached results that are updated periodically in the background
func GetTrendingKind20(db *sql.DB) ([]Post, error) {
if cached, ok := trendingCache.Get("trending_kind_20"); ok {
return cached.([]Post), nil
}
// If cache is empty (which shouldn't happen with background updates),
// calculate on demand
return calculateTrendingKind20(db)
}
// calculateTrendingKind20 performs the actual calculation and updates the cache
func calculateTrendingKind20(db *sql.DB) ([]Post, error) {
query := `
WITH reactions AS (
SELECT
tags_expanded.value->1 #>> '{}' AS original_event_id,
COUNT(*) as reaction_count
FROM event e
CROSS JOIN LATERAL jsonb_array_elements(tags) as tags_expanded(value)
WHERE e.kind::text = '7'
AND e.created_at >= extract(epoch from now() - interval '24 hours')::bigint
AND tags_expanded.value->0 #>> '{}' = 'e'
GROUP BY tags_expanded.value->1 #>> '{}'
)
SELECT
e.id,
e.pubkey,
to_timestamp(e.created_at) as created_at,
e.kind,
e.content,
e.tags,
COALESCE(r.reaction_count, 0) as reaction_count
FROM event e
LEFT JOIN reactions r ON e.id = r.original_event_id
WHERE e.kind::text = '20'
AND e.created_at >= extract(epoch from now() - interval '24 hours')::bigint
ORDER BY reaction_count DESC, e.created_at DESC
LIMIT 20
`
rows, err := db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var trendingPosts []Post
for rows.Next() {
var post Post
var tagsJSON []byte
if err := rows.Scan(&post.ID, &post.PubKey, &post.CreatedAt, &post.Kind, &post.Content, &tagsJSON, &post.ReactionCount); err != nil {
return nil, err
}
if err := json.Unmarshal(tagsJSON, &post.Tags); err != nil {
return nil, err
}
trendingPosts = append(trendingPosts, post)
}
trendingCache.Set("trending_kind_20", trendingPosts, cacheDuration)
return trendingPosts, nil
}

View File

@ -1,79 +0,0 @@
package trending
import (
"database/sql"
"encoding/json"
)
// GetTrendingScoreKind20 returns the top 20 trending posts of kind 20 from the last 24 hours
func GetTrendingScoreKind20(db *sql.DB) ([]Post, error) {
if cached, ok := trendingCache.Get("trending_kind_20"); ok {
return cached.([]Post), nil
}
query := `
WITH engagement AS (
SELECT
tags_expanded.value->1 #>> '{}' AS original_event_id,
e.kind::text,
extract(epoch from now()) - e.created_at as seconds_ago,
COUNT(*) as count
FROM event e
CROSS JOIN LATERAL jsonb_array_elements(tags) as tags_expanded(value)
WHERE e.kind::text IN ('1', '6', '7','9735') -- replies, reposts, reactions, zaps
AND e.created_at >= extract(epoch from now() - interval '24 hours')::bigint
AND tags_expanded.value->0 #>> '{}' = 'e'
GROUP BY tags_expanded.value->1 #>> '{}', e.kind::text, e.created_at
),
scores AS (
SELECT
original_event_id,
SUM(
CASE
WHEN kind = '7' THEN count * 1.0 -- reactions weight
WHEN kind = '6' THEN count * 2.0 -- reposts weight
WHEN kind = '1' THEN count * 1.5 -- replies weight
WHEN kind = '9735' THEN count * 1.3 -- zaps weight
END / power((seconds_ago + 7200) / 3600, 1.8) -- time decay
) as trending_score
FROM engagement
GROUP BY original_event_id
)
SELECT
e.id,
e.pubkey,
to_timestamp(e.created_at) as created_at,
e.kind,
e.content,
e.tags,
COALESCE(s.trending_score, 0.0) as reaction_count -- Cast to float
FROM event e
LEFT JOIN scores s ON e.id = s.original_event_id
WHERE e.kind::text = '20'
AND e.created_at >= extract(epoch from now() - interval '24 hours')::bigint
ORDER BY reaction_count DESC, e.created_at DESC
LIMIT 20
`
rows, err := db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var trendingPosts []Post
for rows.Next() {
var post Post
var tagsJSON []byte
if err := rows.Scan(&post.ID, &post.PubKey, &post.CreatedAt, &post.Kind, &post.Content, &tagsJSON, &post.ReactionCount); err != nil {
return nil, err
}
if err := json.Unmarshal(tagsJSON, &post.Tags); err != nil {
return nil, err
}
trendingPosts = append(trendingPosts, post)
}
trendingCache.Set("trending_kind_20", trendingPosts, cacheDuration)
return trendingPosts, nil
}

View File

@ -1,13 +0,0 @@
package trending
import "time"
type Post struct {
ID string `json:"id"`
PubKey string `json:"pubkey"`
CreatedAt time.Time `json:"created_at"`
Kind int `json:"kind"`
Content string `json:"content"`
Tags [][]string `json:"tags"`
ReactionCount float64 `json:"reaction_count"`
}