Sako
After I enabled parental controls on my android phone, which I may write a post about. I became free from social media and have been searching for other cooler ways to entertain myself. This little stupid blog is one of them. Sako is another.
Sako is one of programs that lets you save web pages and read them later, other tools I know are wallabag and pocket. There are probably more.
you can find sako here https://sr.ht/~sbinet/sako/
I like it mainly because it lets me read articles and posts and whatnot in the same format, since it does not save any css or javascript it’s pure html. I made myself a very minimal webview app as a client for Sako, which wasn’t super hard, I may some day release it on codeberg we will see…
Sako comes with it’s own cool frontend as well which you can acess at localhost:8080
With a bit of effort you can also create an RSS functionality ! which is the most useful feature for me !
this is some go code for this kind of RSS, it’s kind of buggy.
package main import ( "bufio" "bytes" "encoding/json" "fmt" "log" "net/http" "net/url" "os" "path/filepath" "strings" "time" "io" "github.com/mmcdole/gofeed" ) const ( BaseURL = "http://localhost:8080" // Your Sako server URL TokenURL = BaseURL + "/oauth/v2/token" EntriesURL = BaseURL + "/api/entries.json" ClientID = "" ClientSecret = "" Username = "" Password = "" TrackFile = "imported_entries.json" ) // getAccessToken performs the OAuth2 password grant request // and returns the access token. func getAccessToken() (string, error) { values := url.Values{} values.Set("grant_type", "password") values.Set("client_id", ClientID) values.Set("client_secret", ClientSecret) values.Set("username", Username) values.Set("password", Password) resp, err := http.PostForm(TokenURL, values) if err != nil { return "", fmt.Errorf("error requesting token: %v", err) } defer resp.Body.Close() body, err := io.ReadAll(io.Reader(resp.Body)) if err != nil { return "", fmt.Errorf("error reading token response: %v", err) } var tokenResp struct { AccessToken string `json:"access_token"` } if err := json.Unmarshal(body, &tokenResp); err != nil { return "", fmt.Errorf("error parsing token response: %v", err) } if tokenResp.AccessToken == "" { return "", fmt.Errorf("no access token in response: %s", string(body)) } log.Println(tokenResp.AccessToken) return tokenResp.AccessToken, nil } // loadFeedURLs reads RSS feed URLs from $HOME/.config/sako/feeds.txt. func loadFeedURLs() ([]string, error) { homeDir, err := os.UserHomeDir() if err != nil { return nil, err } feedsPath := filepath.Join(homeDir, ".config", "sako", "feeds.txt") file, err := os.Open(feedsPath) if err != nil { return nil, fmt.Errorf("failed to open feeds file at %s: %v", feedsPath, err) } defer file.Close() var feeds []string scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line != "" { feeds = append(feeds, line) } } if err := scanner.Err(); err != nil { return nil, err } return feeds, nil } // loadImportedIDs reads a JSON array of imported IDs from TrackFile. func loadImportedIDs() map[string]bool { imported := make(map[string]bool) homeDir, err := os.UserHomeDir() if err != nil { return nil } importedFile := filepath.Join(homeDir, ".config", "sako", TrackFile) data, err := os.ReadFile(importedFile) if err != nil { // File might not exist initially, so return an empty map. return imported } var ids []string if err := json.Unmarshal(data, &ids); err != nil { log.Printf("Error parsing %s: %v", TrackFile, err) return imported } for _, id := range ids { imported[id] = true } return imported } // saveImportedIDs writes the imported IDs to TrackFile. func saveImportedIDs(imported map[string]bool) error { var ids []string for id := range imported { ids = append(ids, id) } data, err := json.Marshal(ids) if err != nil { return err } homeDir, err := os.UserHomeDir() if err != nil { return nil } importedFile := filepath.Join(homeDir, ".config", "sako", TrackFile) return os.WriteFile(importedFile, data, 0644) } // processFeed fetches and processes one RSS feed. func processFeed(feedURL string, importedIDs map[string]bool, client *http.Client, fp *gofeed.Parser, token string) { feed, err := fp.ParseURL(feedURL) if err != nil { log.Printf("Error parsing feed %s: %v", feedURL, err) return } for _, item := range feed.Items { // Use feed URL combined with item's GUID (or Link if GUID is empty) as a unique identifier. id := feedURL + "::" + item.GUID if id == feedURL+"::" { id = feedURL + "::" + item.Link } // Skip if already imported. if importedIDs[id] { continue } // Build JSON payload. payload := map[string]interface{}{ "url": item.Link, } // TODO this should be more generic, like some whitelist that does this if strings.Contains(item.Link, "posteo") { payload = map[string]interface{}{ "url": item.Link, "title": item.Title, "content": item.Content, } } jsonData, err := json.Marshal(payload) if err != nil { log.Printf("Error marshaling JSON for %s: %v", item.Title, err) continue } log.Println(bytes.NewBuffer(jsonData)) // Create POST request. req, err := http.NewRequest("POST", EntriesURL, bytes.NewBuffer(jsonData)) if err != nil { log.Printf("Error creating request for %s: %v", item.Title, err) continue } req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") // Send the request. resp, err := client.Do(req) if err != nil { log.Printf("Error posting %s: %v", item.Title, err) continue } resp.Body.Close() // Check for success. if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated { log.Printf("Imported: %s", item.Title) importedIDs[id] = true } else { log.Printf("Failed to import %s: HTTP %d", item.Title, resp.StatusCode) } } } func Run() { // Retrieve an access token using OAuth2. token, err := getAccessToken() if err != nil { log.Fatalf("Error getting access token: %v", err) } log.Printf("Obtained access token.") // Load feed URLs from the configuration file. feedURLs, err := loadFeedURLs() if err != nil { log.Fatalf("Error loading feeds: %v", err) } if len(feedURLs) == 0 { log.Fatalf("No feeds found in configuration. Please add feed URLs to $HOME/.config/sako/feeds.txt") } // Load previously imported entries. importedIDs := loadImportedIDs() fp := gofeed.NewParser() client := &http.Client{ Timeout: 10 * time.Second, } // Process each RSS feed. for _, feedURL := range feedURLs { processFeed(feedURL, importedIDs, client, fp, token) } // Save updated tracking info. if err := saveImportedIDs(importedIDs); err != nil { log.Printf("Error saving imported IDs: %v", err) } fmt.Println("RSS feed import complete.") }
This code was mostly generated with an LLM(bullshit generator), which is another thing after the phone I have to get rid off in my life.