appview/ingester: ingest sh.tangled.string from the firehose #414

merged
opened by oppi.li targeting master from push-potvrpwlpwsl
Changed files
+165 -21
appview
middleware
pages
templates
strings
fragments
state
knotserver
+56
appview/ingester.go
···
err = i.ingestSpindleMember(e)
case tangled.SpindleNSID:
err = i.ingestSpindle(e)
+
case tangled.StringNSID:
+
err = i.ingestString(e)
}
l = i.Logger.With("nsid", e.Commit.Collection)
}
···
return nil
}
+
+
func (i *Ingester) ingestString(e *models.Event) error {
+
did := e.Did
+
rkey := e.Commit.RKey
+
+
var err error
+
+
l := i.Logger.With("handler", "ingestString", "nsid", e.Commit.Collection, "did", did, "rkey", rkey)
+
l.Info("ingesting record")
+
+
ddb, ok := i.Db.Execer.(*db.DB)
+
if !ok {
+
return fmt.Errorf("failed to index string record, invalid db cast")
+
}
+
+
switch e.Commit.Operation {
+
case models.CommitOperationCreate, models.CommitOperationUpdate:
+
raw := json.RawMessage(e.Commit.Record)
+
record := tangled.String{}
+
err = json.Unmarshal(raw, &record)
+
if err != nil {
+
l.Error("invalid record", "err", err)
+
return err
+
}
+
+
string := db.StringFromRecord(did, rkey, record)
+
+
if err = string.Validate(); err != nil {
+
l.Error("invalid record", "err", err)
+
return err
+
}
+
+
if err = db.AddString(ddb, string); err != nil {
+
l.Error("failed to add string", "err", err)
+
return err
+
}
+
+
return nil
+
+
case models.CommitOperationDelete:
+
if err := db.DeleteString(
+
ddb,
+
db.FilterEq("did", did),
+
db.FilterEq("rkey", rkey),
+
); err != nil {
+
l.Error("failed to delete", "err", err)
+
return fmt.Errorf("failed to delete string record: %w", err)
+
}
+
+
return nil
+
}
+
+
return nil
+
}
+2 -10
appview/middleware/middleware.go
···
}
}
-
func StripLeadingAt(next http.Handler) http.Handler {
-
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
-
path := req.URL.EscapedPath()
-
if strings.HasPrefix(path, "/@") {
-
req.URL.RawPath = "/" + strings.TrimPrefix(path, "/@")
-
}
-
next.ServeHTTP(w, req)
-
})
-
}
-
func (mw Middleware) ResolveIdent() middlewareFunc {
excluded := []string{"favicon.ico"}
···
return
}
+
didOrHandle = strings.TrimPrefix(didOrHandle, "@")
+
id, err := mw.idResolver.ResolveIdent(req.Context(), didOrHandle)
if err != nil {
// invalid did or handle
+89
appview/pages/templates/strings/fragments/form.html
···
+
{{ define "strings/fragments/form" }}
+
<form
+
{{ if eq .Action "new" }}
+
hx-post="/strings/new"
+
{{ else }}
+
hx-post="/strings/{{.String.Did}}/{{.String.Rkey}}/edit"
+
{{ end }}
+
hx-indicator="#new-button"
+
class="p-6 pb-4 dark:text-white flex flex-col gap-2 bg-white dark:bg-gray-800 drop-shadow-sm rounded"
+
hx-swap="none">
+
<div class="flex flex-col md:flex-row md:items-center gap-2">
+
<input
+
type="text"
+
id="filename"
+
name="filename"
+
placeholder="Filename with extension"
+
required
+
value="{{ .String.Filename }}"
+
class="md:max-w-64 dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400 px-3 py-2 border rounded"
+
>
+
<input
+
type="text"
+
id="description"
+
name="description"
+
value="{{ .String.Description }}"
+
placeholder="Description ..."
+
class="flex-1 dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400 px-3 py-2 border rounded"
+
>
+
</div>
+
<textarea
+
name="content"
+
id="content-textarea"
+
wrap="off"
+
class="w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400"
+
rows="20"
+
placeholder="Paste your string here!"
+
required>{{ .String.Contents }}</textarea>
+
<div class="flex justify-between items-center">
+
<div id="content-stats" class="text-sm text-gray-500 dark:text-gray-400">
+
<span id="line-count">0 lines</span>
+
<span class="select-none px-1 [&:before]:content-['·']"></span>
+
<span id="byte-count">0 bytes</span>
+
</div>
+
<div id="actions" class="flex gap-2 items-center">
+
{{ if eq .Action "edit" }}
+
<a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 "
+
href="/strings/{{ .String.Did }}/{{ .String.Rkey }}">
+
{{ i "x" "size-4" }}
+
<span class="hidden md:inline">cancel</span>
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
+
</a>
+
{{ end }}
+
<button
+
type="submit"
+
id="new-button"
+
class="w-fit btn-create rounded flex items-center py-0 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 group"
+
>
+
<span class="inline-flex items-center gap-2">
+
{{ i "arrow-up" "w-4 h-4" }}
+
publish
+
</span>
+
<span class="pl-2 hidden group-[.htmx-request]:inline">
+
{{ i "loader-circle" "w-4 h-4 animate-spin" }}
+
</span>
+
</button>
+
</div>
+
</div>
+
<script>
+
(function() {
+
const textarea = document.getElementById('content-textarea');
+
const lineCount = document.getElementById('line-count');
+
const byteCount = document.getElementById('byte-count');
+
function updateStats() {
+
const content = textarea.value;
+
const lines = content === '' ? 0 : content.split('\n').length;
+
const bytes = new TextEncoder().encode(content).length;
+
lineCount.textContent = `${lines} line${lines !== 1 ? 's' : ''}`;
+
byteCount.textContent = `${bytes} byte${bytes !== 1 ? 's' : ''}`;
+
}
+
textarea.addEventListener('input', updateStats);
+
textarea.addEventListener('paste', () => {
+
setTimeout(updateStats, 0);
+
});
+
updateStats();
+
})();
+
</script>
+
<div id="error" class="error dark:text-red-400"></div>
+
</form>
+
{{ end }}
+17
appview/pages/templates/strings/put.html
···
+
{{ define "title" }}publish a new string{{ end }}
+
+
{{ define "topbar" }}
+
{{ template "layouts/topbar" $ }}
+
{{ end }}
+
+
{{ define "content" }}
+
<div class="px-6 py-2 mb-4">
+
{{ if eq .Action "new" }}
+
<p class="text-xl font-bold dark:text-white">Create a new string</p>
+
<p class="">Store and share code snippets with ease.</p>
+
{{ else }}
+
<p class="text-xl font-bold dark:text-white">Edit string</p>
+
{{ end }}
+
</div>
+
{{ template "strings/fragments/form" . }}
+
{{ end }}
-3
appview/state/router.go
···
func (s *State) UserRouter(mw *middleware.Middleware) http.Handler {
r := chi.NewRouter()
-
// strip @ from user
-
r.Use(middleware.StripLeadingAt)
-
r.With(mw.ResolveIdent()).Route("/{user}", func(r chi.Router) {
r.Get("/", s.Profile)
+1
appview/state/state.go
···
tangled.ActorProfileNSID,
tangled.SpindleMemberNSID,
tangled.SpindleNSID,
+
tangled.StringNSID,
},
nil,
slog.Default(),
-8
knotserver/file.go
···
"tangled.sh/tangled.sh/core/types"
)
-
func (h *Handle) listFiles(files []types.NiceTree, data map[string]any, w http.ResponseWriter) {
-
data["files"] = files
-
-
writeJSON(w, data)
-
return
-
}
-
func countLines(r io.Reader) (int, error) {
buf := make([]byte, 32*1024)
bufLen := 0
···
resp.Lines = lc
writeJSON(w, resp)
-
return
}