forked from tangled.org/core
Monorepo for Tangled

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

Signed-off-by: oppiliappan <me@oppi.li>

authored by oppi.li and committed by Tangled 375bfcd8 099d718d

Changed files
+165 -21
appview
middleware
pages
templates
strings
fragments
state
knotserver
+56
appview/ingester.go
··· 64 64 err = i.ingestSpindleMember(e) 65 65 case tangled.SpindleNSID: 66 66 err = i.ingestSpindle(e) 67 + case tangled.StringNSID: 68 + err = i.ingestString(e) 67 69 } 68 70 l = i.Logger.With("nsid", e.Commit.Collection) 69 71 } ··· 549 551 550 552 return nil 551 553 } 554 + 555 + func (i *Ingester) ingestString(e *models.Event) error { 556 + did := e.Did 557 + rkey := e.Commit.RKey 558 + 559 + var err error 560 + 561 + l := i.Logger.With("handler", "ingestString", "nsid", e.Commit.Collection, "did", did, "rkey", rkey) 562 + l.Info("ingesting record") 563 + 564 + ddb, ok := i.Db.Execer.(*db.DB) 565 + if !ok { 566 + return fmt.Errorf("failed to index string record, invalid db cast") 567 + } 568 + 569 + switch e.Commit.Operation { 570 + case models.CommitOperationCreate, models.CommitOperationUpdate: 571 + raw := json.RawMessage(e.Commit.Record) 572 + record := tangled.String{} 573 + err = json.Unmarshal(raw, &record) 574 + if err != nil { 575 + l.Error("invalid record", "err", err) 576 + return err 577 + } 578 + 579 + string := db.StringFromRecord(did, rkey, record) 580 + 581 + if err = string.Validate(); err != nil { 582 + l.Error("invalid record", "err", err) 583 + return err 584 + } 585 + 586 + if err = db.AddString(ddb, string); err != nil { 587 + l.Error("failed to add string", "err", err) 588 + return err 589 + } 590 + 591 + return nil 592 + 593 + case models.CommitOperationDelete: 594 + if err := db.DeleteString( 595 + ddb, 596 + db.FilterEq("did", did), 597 + db.FilterEq("rkey", rkey), 598 + ); err != nil { 599 + l.Error("failed to delete", "err", err) 600 + return fmt.Errorf("failed to delete string record: %w", err) 601 + } 602 + 603 + return nil 604 + } 605 + 606 + return nil 607 + }
+2 -10
appview/middleware/middleware.go
··· 167 167 } 168 168 } 169 169 170 - func StripLeadingAt(next http.Handler) http.Handler { 171 - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 172 - path := req.URL.EscapedPath() 173 - if strings.HasPrefix(path, "/@") { 174 - req.URL.RawPath = "/" + strings.TrimPrefix(path, "/@") 175 - } 176 - next.ServeHTTP(w, req) 177 - }) 178 - } 179 - 180 170 func (mw Middleware) ResolveIdent() middlewareFunc { 181 171 excluded := []string{"favicon.ico"} 182 172 ··· 187 177 next.ServeHTTP(w, req) 188 178 return 189 179 } 180 + 181 + didOrHandle = strings.TrimPrefix(didOrHandle, "@") 190 182 191 183 id, err := mw.idResolver.ResolveIdent(req.Context(), didOrHandle) 192 184 if err != nil {
+89
appview/pages/templates/strings/fragments/form.html
··· 1 + {{ define "strings/fragments/form" }} 2 + <form 3 + {{ if eq .Action "new" }} 4 + hx-post="/strings/new" 5 + {{ else }} 6 + hx-post="/strings/{{.String.Did}}/{{.String.Rkey}}/edit" 7 + {{ end }} 8 + hx-indicator="#new-button" 9 + class="p-6 pb-4 dark:text-white flex flex-col gap-2 bg-white dark:bg-gray-800 drop-shadow-sm rounded" 10 + hx-swap="none"> 11 + <div class="flex flex-col md:flex-row md:items-center gap-2"> 12 + <input 13 + type="text" 14 + id="filename" 15 + name="filename" 16 + placeholder="Filename with extension" 17 + required 18 + value="{{ .String.Filename }}" 19 + 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" 20 + > 21 + <input 22 + type="text" 23 + id="description" 24 + name="description" 25 + value="{{ .String.Description }}" 26 + placeholder="Description ..." 27 + class="flex-1 dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400 px-3 py-2 border rounded" 28 + > 29 + </div> 30 + <textarea 31 + name="content" 32 + id="content-textarea" 33 + wrap="off" 34 + class="w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400" 35 + rows="20" 36 + placeholder="Paste your string here!" 37 + required>{{ .String.Contents }}</textarea> 38 + <div class="flex justify-between items-center"> 39 + <div id="content-stats" class="text-sm text-gray-500 dark:text-gray-400"> 40 + <span id="line-count">0 lines</span> 41 + <span class="select-none px-1 [&:before]:content-['·']"></span> 42 + <span id="byte-count">0 bytes</span> 43 + </div> 44 + <div id="actions" class="flex gap-2 items-center"> 45 + {{ if eq .Action "edit" }} 46 + <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 " 47 + href="/strings/{{ .String.Did }}/{{ .String.Rkey }}"> 48 + {{ i "x" "size-4" }} 49 + <span class="hidden md:inline">cancel</span> 50 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 51 + </a> 52 + {{ end }} 53 + <button 54 + type="submit" 55 + id="new-button" 56 + class="w-fit btn-create rounded flex items-center py-0 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 group" 57 + > 58 + <span class="inline-flex items-center gap-2"> 59 + {{ i "arrow-up" "w-4 h-4" }} 60 + publish 61 + </span> 62 + <span class="pl-2 hidden group-[.htmx-request]:inline"> 63 + {{ i "loader-circle" "w-4 h-4 animate-spin" }} 64 + </span> 65 + </button> 66 + </div> 67 + </div> 68 + <script> 69 + (function() { 70 + const textarea = document.getElementById('content-textarea'); 71 + const lineCount = document.getElementById('line-count'); 72 + const byteCount = document.getElementById('byte-count'); 73 + function updateStats() { 74 + const content = textarea.value; 75 + const lines = content === '' ? 0 : content.split('\n').length; 76 + const bytes = new TextEncoder().encode(content).length; 77 + lineCount.textContent = `${lines} line${lines !== 1 ? 's' : ''}`; 78 + byteCount.textContent = `${bytes} byte${bytes !== 1 ? 's' : ''}`; 79 + } 80 + textarea.addEventListener('input', updateStats); 81 + textarea.addEventListener('paste', () => { 82 + setTimeout(updateStats, 0); 83 + }); 84 + updateStats(); 85 + })(); 86 + </script> 87 + <div id="error" class="error dark:text-red-400"></div> 88 + </form> 89 + {{ end }}
+17
appview/pages/templates/strings/put.html
··· 1 + {{ define "title" }}publish a new string{{ end }} 2 + 3 + {{ define "topbar" }} 4 + {{ template "layouts/topbar" $ }} 5 + {{ end }} 6 + 7 + {{ define "content" }} 8 + <div class="px-6 py-2 mb-4"> 9 + {{ if eq .Action "new" }} 10 + <p class="text-xl font-bold dark:text-white">Create a new string</p> 11 + <p class="">Store and share code snippets with ease.</p> 12 + {{ else }} 13 + <p class="text-xl font-bold dark:text-white">Edit string</p> 14 + {{ end }} 15 + </div> 16 + {{ template "strings/fragments/form" . }} 17 + {{ end }}
-3
appview/state/router.go
··· 68 68 func (s *State) UserRouter(mw *middleware.Middleware) http.Handler { 69 69 r := chi.NewRouter() 70 70 71 - // strip @ from user 72 - r.Use(middleware.StripLeadingAt) 73 - 74 71 r.With(mw.ResolveIdent()).Route("/{user}", func(r chi.Router) { 75 72 r.Get("/", s.Profile) 76 73
+1
appview/state/state.go
··· 93 93 tangled.ActorProfileNSID, 94 94 tangled.SpindleMemberNSID, 95 95 tangled.SpindleNSID, 96 + tangled.StringNSID, 96 97 }, 97 98 nil, 98 99 slog.Default(),
-8
knotserver/file.go
··· 10 10 "tangled.sh/tangled.sh/core/types" 11 11 ) 12 12 13 - func (h *Handle) listFiles(files []types.NiceTree, data map[string]any, w http.ResponseWriter) { 14 - data["files"] = files 15 - 16 - writeJSON(w, data) 17 - return 18 - } 19 - 20 13 func countLines(r io.Reader) (int, error) { 21 14 buf := make([]byte, 32*1024) 22 15 bufLen := 0 ··· 52 45 53 46 resp.Lines = lc 54 47 writeJSON(w, resp) 55 - return 56 48 }