3 Commits

7 changed files with 92 additions and 122 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>

49
relay/cache/cache.go vendored
View File

@ -8,11 +8,13 @@ import (
type item struct {
value interface{}
expiration int64
staleTime int64
}
type Cache struct {
items map[string]item
mu sync.RWMutex
items map[string]item
mu sync.RWMutex
refreshing sync.Map
}
func New() *Cache {
@ -27,35 +29,68 @@ func (c *Cache) Set(key string, value interface{}, duration time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
now := time.Now().UnixNano()
c.items[key] = item{
value: value,
expiration: time.Now().Add(duration).UnixNano(),
expiration: now + duration.Nanoseconds(),
staleTime: now + (duration * 2).Nanoseconds(), // Stale time is double the normal duration
}
}
func (c *Cache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
item, exists := c.items[key]
c.mu.RUnlock()
if !exists {
return nil, false
}
if time.Now().UnixNano() > item.expiration {
now := time.Now().UnixNano()
if now > item.staleTime {
return nil, false
}
return item.value, true
}
func (c *Cache) GetOrRefresh(key string, refreshFn func() (interface{}, error), duration time.Duration) (interface{}, bool) {
// Try to get from cache first
if value, exists := c.Get(key); exists {
now := time.Now().UnixNano()
item := c.items[key]
// If the value is expired but not stale, trigger refresh in background
if now > item.expiration && now <= item.staleTime {
if _, loading := c.refreshing.LoadOrStore(key, true); !loading {
go func() {
defer c.refreshing.Delete(key)
if newValue, err := refreshFn(); err == nil {
c.Set(key, newValue, duration)
}
}()
}
}
return value, true
}
// If no value exists, do a blocking refresh
value, err := refreshFn()
if err != nil {
return nil, false
}
c.Set(key, value, duration)
return value, true
}
func (c *Cache) startCleanup() {
ticker := time.NewTicker(time.Minute)
for range ticker.C {
c.mu.Lock()
now := time.Now().UnixNano()
for k, v := range c.items {
if now > v.expiration {
if now > v.staleTime {
delete(c.items, k)
}
}

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)
@ -252,7 +254,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

@ -8,17 +8,22 @@ import (
"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 = 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
}
func fetchTrendingKind20(db *sql.DB) ([]Post, error) {
query := `
WITH reactions AS (
SELECT
@ -66,6 +71,17 @@ func GetTrendingBasicKind20(db *sql.DB) ([]Post, error) {
trendingPosts = append(trendingPosts, post)
}
trendingCache.Set("trending_kind_20", trendingPosts, cacheDuration)
return trendingPosts, nil
}
func GetTrendingKind20(db *sql.DB) ([]Post, error) {
posts, exists := trendingCache.GetOrRefresh("trending_kind_20", func() (interface{}, error) {
return fetchTrendingKind20(db)
}, cacheDuration)
if !exists {
return nil, nil
}
return posts.([]Post), 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"`
}