Compare commits

...

2 Commits

Author SHA1 Message Date
8d66e3decc app refactor 2024-08-06 15:35:52 -04:00
df361ea437 import events 2024-08-06 10:34:53 -04:00
8 changed files with 312 additions and 42 deletions

162
app/src/api/importEvents.go Normal file
View File

@ -0,0 +1,162 @@
package api
import (
"context"
"encoding/json"
"fmt"
"html/template"
"io"
"log"
"net/http"
"strings"
"grain/server/db"
relay "grain/server/types"
"go.mongodb.org/mongo-driver/mongo"
"golang.org/x/net/websocket"
)
type ResultData struct {
Success bool
Message string
Count int
}
func ImportEvents(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
return
}
pubkey := r.FormValue("pubkey")
relayUrls := r.FormValue("relayUrls")
urls := strings.Split(relayUrls, ",")
var totalEvents int
var errorMessage string
for _, url := range urls {
events, err := fetchEventsFromRelay(pubkey, url)
if err != nil {
log.Printf("Error fetching events from relay %s: %v", url, err)
errorMessage = fmt.Sprintf("Error fetching events from relay %s", url)
renderResult(w, false, errorMessage, 0)
return
}
err = storeEvents(events)
if err != nil {
log.Printf("Error storing events: %v", err)
errorMessage = "Error storing events"
renderResult(w, false, errorMessage, 0)
return
}
totalEvents += len(events)
}
renderResult(w, true, "Events imported successfully", totalEvents)
}
func renderResult(w http.ResponseWriter, success bool, message string, count int) {
tmpl, err := template.New("result").Parse(`
{{ if .Success }}
<p class="text-green-500">Successfully inserted {{ .Count }} events.</p>
{{ else }}
<p class="text-red-500">Failed to import events: {{ .Message }}</p>
{{ end }}
`)
if err != nil {
http.Error(w, "Error generating result", http.StatusInternalServerError)
return
}
data := ResultData{
Success: success,
Message: message,
Count: count,
}
if err := tmpl.Execute(w, data); err != nil {
http.Error(w, "Error rendering result", http.StatusInternalServerError)
}
}
func fetchEventsFromRelay(pubkey, relayUrl string) ([]relay.Event, error) {
log.Printf("Connecting to relay: %s", relayUrl)
conn, err := websocket.Dial(relayUrl, "", "http://localhost/")
if err != nil {
log.Printf("Error connecting to relay %s: %v", relayUrl, err)
return nil, err
}
defer conn.Close()
log.Printf("Connected to relay: %s", relayUrl)
reqMessage := fmt.Sprintf(`["REQ", "import-sub", {"authors": ["%s"]}]`, pubkey)
log.Printf("Sending request: %s", reqMessage)
if _, err := conn.Write([]byte(reqMessage)); err != nil {
log.Printf("Error sending request to relay %s: %v", relayUrl, err)
return nil, err
}
var events []relay.Event
for {
var msg []byte
if err := websocket.Message.Receive(conn, &msg); err != nil {
if err == io.EOF {
break
}
log.Printf("Error receiving message from relay %s: %v", relayUrl, err)
return nil, err
}
log.Printf("Received message: %s", string(msg))
var response []interface{}
if err := json.Unmarshal(msg, &response); err != nil {
log.Printf("Error unmarshaling message from relay %s: %v", relayUrl, err)
return nil, err
}
if response[0] == "EOSE" {
log.Printf("Received EOSE message from relay %s, closing connection", relayUrl)
break
}
if response[0] == "EVENT" {
eventData, err := json.Marshal(response[2]) // Change index from 1 to 2
if err != nil {
log.Printf("Error marshaling event data from relay %s: %v", relayUrl, err)
continue
}
var event relay.Event
if err := json.Unmarshal(eventData, &event); err != nil {
log.Printf("Error unmarshaling event data from relay %s: %v", relayUrl, err)
continue
}
events = append(events, event)
}
}
log.Printf("Fetched %d events from relay %s", len(events), relayUrl)
return events, nil
}
func storeEvents(events []relay.Event) error {
for _, event := range events {
collection := db.GetCollection(event.Kind)
_, err := collection.InsertOne(context.TODO(), event)
if err != nil {
if mongo.IsDuplicateKeyError(err) {
log.Printf("Duplicate event ID: %s for event kind: %d", event.ID, event.Kind)
} else {
log.Printf("Error inserting event with ID: %s for event kind: %d: %v", event.ID, event.Kind, err)
return err
}
} else {
log.Printf("Successfully inserted event with ID: %s for event kind: %d", event.ID, event.Kind)
}
}
return nil
}

View File

@ -1,15 +1,10 @@
package app package app
import ( import (
"context"
"grain/server/db" "grain/server/db"
relay "grain/server/types" relay "grain/server/types"
"html/template" "html/template"
"net/http" "net/http"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
) )
type PageData struct { type PageData struct {
@ -78,38 +73,4 @@ func PrependDir(dir string, files []string) []string {
return fullPaths return fullPaths
} }
// FetchTopTenRecentEvents queries the database and returns the top ten most recent events.
func FetchTopTenRecentEvents(client *mongo.Client) ([]relay.Event, error) {
var results []relay.Event
collections, err := client.Database("grain").ListCollectionNames(context.TODO(), bson.M{})
if err != nil {
return nil, err
}
for _, collectionName := range collections {
collection := client.Database("grain").Collection(collectionName)
filter := bson.D{}
opts := options.Find().SetSort(bson.D{{Key: "createdat", Value: -1}}).SetLimit(10)
cursor, err := collection.Find(context.TODO(), filter, opts)
if err != nil {
return nil, err
}
defer cursor.Close(context.TODO())
for cursor.Next(context.TODO()) {
var event relay.Event
if err := cursor.Decode(&event); err != nil {
return nil, err
}
results = append(results, event)
}
if err := cursor.Err(); err != nil {
return nil, err
}
}
return results, nil
}

View File

@ -0,0 +1,47 @@
package app
import (
"context"
relay "grain/server/types"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
// FetchTopTenRecentEvents queries the database and returns the top ten most recent events.
func FetchTopTenRecentEvents(client *mongo.Client) ([]relay.Event, error) {
var results []relay.Event
collections, err := client.Database("grain").ListCollectionNames(context.TODO(), bson.M{})
if err != nil {
return nil, err
}
for _, collectionName := range collections {
collection := client.Database("grain").Collection(collectionName)
filter := bson.D{}
opts := options.Find().SetSort(bson.D{{Key: "createdat", Value: -1}}).SetLimit(10)
cursor, err := collection.Find(context.TODO(), filter, opts)
if err != nil {
return nil, err
}
defer cursor.Close(context.TODO())
for cursor.Next(context.TODO()) {
var event relay.Event
if err := cursor.Decode(&event); err != nil {
return nil, err
}
results = append(results, event)
}
if err := cursor.Err(); err != nil {
return nil, err
}
}
return results, nil
}

View File

@ -0,0 +1,15 @@
package routes
import (
app "grain/app/src"
"net/http"
)
func ImportEvents(w http.ResponseWriter, r *http.Request) {
data := app.PageData{
Title: "Import Events",
}
// Call RenderTemplate with the specific template for this route
app.RenderTemplate(w, data, "importEvents.html")
}

View File

@ -0,0 +1,53 @@
{{define "view"}}
<main class="flex flex-col items-center justify-center p-8">
<div class="mb-4">You are now viewing the {{.Title}}</div>
<div
class="container flex justify-center p-4 border-2 border-gray-600 border-solid rounded-md w-fit"
>
<form
id="import-form"
hx-post="/import-results"
hx-target="#result"
hx-indicator="#spinner"
>
<div>
<label for="pubkey">Pubkey:</label>
<input
class="p-2 m-2 text-black rounded-md"
type="text"
id="pubkey"
name="pubkey"
required
maxlength="64"
/>
</div>
<div>
<label for="relayUrls">Relay URLs (comma separated):</label>
<input
class="p-2 m-2 text-black rounded-md"
type="text"
id="relayUrls"
name="relayUrls"
required
/>
</div>
<button
class="p-2 m-2 font-bold bg-green-500 rounded-md font-xl"
type="submit"
>
Import Events
</button>
</form>
</div>
<div id="spinner" class="spinner"></div>
<div id="result" class="p-2 m-2 text-xl font-bold"></div>
<button
hx-get="/"
hx-swap="outerHTML"
hx-target="body"
class="p-2 m-2 text-white bg-blue-400 rounded-md"
>
Return to Dashboard
</button>
</main>
{{end}}

View File

@ -1,7 +1,7 @@
{{define "view"}} {{define "view"}}
<main class="flex flex-col items-center justify-center p-8"> <main class="flex flex-col items-center justify-center p-8">
<div class="mb-4">You are now viewing the {{.Title}}</div> <div class="mb-4">You are now viewing the {{.Title}}</div>
<h2>Top Ten Most Recent Events</h2> <!--<h2>Top Ten Most Recent Events</h2>
<ul> <ul>
{{range .Events}} {{range .Events}}
<li> <li>
@ -11,6 +11,14 @@
<strong>PubKey:</strong> {{.PubKey}} <strong>PubKey:</strong> {{.PubKey}}
</li> </li>
{{end}} {{end}}
</ul> </ul>-->
<button
hx-get="/import-events"
hx-swap="outerHTML"
class="p-2 mb-2 text-white bg-orange-400 rounded-md"
hx-target="body"
>
Import Events
</button>
</main> </main>
{{end}} {{end}}

View File

@ -26,6 +26,26 @@
<link href="/static/custom.min.css" rel="stylesheet" /> <link href="/static/custom.min.css" rel="stylesheet" />
<link rel="icon" href="/static/img/favicon.ico" type="image/x-icon" /> <link rel="icon" href="/static/img/favicon.ico" type="image/x-icon" />
<title>{{.Title}}</title> <title>{{.Title}}</title>
<style>
.spinner {
display: none;
width: 50px;
height: 50px;
border: 5px solid lightgray;
border-top: 5px solid blue;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
</head> </head>
<body class="font-mono text-center text-textPrimary bg-bgPrimary"> <body class="font-mono text-center text-textPrimary bg-bgPrimary">
{{template "header" .}} {{template "view" .}} {{template "footer" .}} {{template "header" .}} {{template "view" .}} {{template "footer" .}}

View File

@ -2,7 +2,9 @@ package main
import ( import (
"fmt" "fmt"
"grain/app" app "grain/app/src"
"grain/app/src/api"
"grain/app/src/routes"
"grain/config" "grain/config"
configTypes "grain/config/types" configTypes "grain/config/types"
relay "grain/server" relay "grain/server"
@ -47,6 +49,8 @@ func main() {
func setupRoutes() *http.ServeMux { func setupRoutes() *http.ServeMux {
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("/", ListenAndServe) mux.HandleFunc("/", ListenAndServe)
mux.HandleFunc("/import-results", api.ImportEvents)
mux.HandleFunc("/import-events", routes.ImportEvents)
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("app/static")))) mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("app/static"))))
mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "app/static/img/favicon.ico") http.ServeFile(w, r, "app/static/img/favicon.ico")