2025-01-28

Adolescent DeepSeek on a Mac laptop


TL; DR: DeepSeek-R1-14B seems pretty "smart". For tasks that, um, don't involve syllable counting, it might be sophisticated enough to be useful. (Or not! It'd take more experimentation to decide.)

But it runs too slowly on my 5-year-old Intel-based MacBookPro, even tricked out with 64GB of RAM. Smaller models run with better performance, but the result quality is likely too poor to be useful. For whatever I do with LLMs, I'm afraid I'm stuck with the online services for now.


In the previous post I ran the "baby" DeepSeek generative AI model on my Mac laptop.

(For instructions on how I installed ollama, and information about its basic use, see that previous post and its postscript.)

The model ran fine, used a lot of CPU but not a lot of memory, generated output at a reasonable pace. Unfortunately, that model is not so "smart". It tells you how it's thinking, and it's not thinking very well. It misunderstands simple things and draws poor inferences.

So now I've decided to try out a still small but bigger, less toy-ish model. The baby model had 1.5B parameters. This one has 14B, so almost 10 times as big.

Downloading the model was trivial with ollama:

% ollama pull deepseek-r1:14b

It's actually a bit less 10 times as big as the baby model, which was about 1GB. Presumably there's some shared, fixed infrastructure, beyond the parameters. (I'm storage constrained, so I'm not retaining models in ~/.ollama, there's just the one model there.)

% du -sh ~/.ollama
8.4G	/Users/swaldman/.ollama

Running the model was also trivial:

% ollama run deepseek-r1:14b
>>> Send a message (/? for help)

I first tried the same prompt as yesterday, "Write a 'Hello, world!' haiku."

The model did run, but...

  1. It ran very, very slowly. The "baby" model ran quite briskly on my laptop. The 14B-parameter version ran like syrup.

  2. The compute load was similar (5-6 full cores saturated), but this model also occupied ~10GB of memory when running. The 1.5B-parameter version used too little memory for me to notice or care.

The 1.5B-parameter model failed to generate a haiku before I grew frustrated and quit it. The 14B-parameter also ultimately failed.

I think I chose a hard "Hello, world!" prompt for all of these models! Haikus are defined by lines with set numbers of syllables. The 1.5B-parameter model mistook letters for syllables. The 14B parameter mostly (but not always!) correctly identified syllables, but often miscounted them.

Even though it too ultimately went 'round and 'round until I interrupted it, the 14B-parameter model's <think>...</think> process was much more relevant and coherent than the 1.5B-parameter model:

>>> Write a 'Hello, world!' haiku.
<think>
Okay, so I need to write a 'Hello, world!' haiku. Hmm, I remember that a haiku is a traditional Japanese poem with three lines. The structure is usually 5 syllables in the first line, then 7 syllables in the second, and ending with another 5 syllables. 

Alright, so the theme here is "Hello, world!" which is pretty much about greeting the world or introducing oneself. I should think of imagery that represents a welcoming or introductory moment.

Let me brainstorm some words related to saying hello and the world. Maybe morning, sunrise, nature sounds like a good setting for a greeting. Dawn could symbolize a new beginning or a fresh start, which fits with saying hello to the world.

For the first line, I want something that captures attention and sets the scene. "Hello, gentle dawn" comes to mind. That's 5 syllables: He-ll-o, gen-tle, dawn. Yep, that works.

Next, the second line needs to be 7 syllables and connect with the first line while expanding on the theme. I want something that shows interaction or response from the world. Maybe "The world whispers back." Let's count: The -world-whis-pers-back. That's 5 syllables? Wait, no. Let me 
break it down.

"The" (1), "world" (2), "whis-pers" (3), "back" (4). Oh, only 4 syllables. Hmm, I need three more. Maybe rephrase it. How about "The world hums a welcome"? That's 6 syllables: The-world-hums-a-wel-come. Wait, that might be stretching it.

Alternatively, "The world greets me softly." Let's see: the (1), world (2), greets (3), me (4), soft-ly (5). No, still not 7. Maybe "The world responds with a smile." That's longer but syllables-wise: The (1), world (2), re-sponds (3), with (4), a (5), smile (6). Still only 6.

Wait, maybe I'm overcomplicating it. Let me think of the action after the hello. Maybe something like "A new day begins." But that's more about the day starting. Alternatively, "Echoes of greeting fill the air." That's longer than 7 syllables though.

Hmm, perhaps "The world says hello too." Wait, let me count: The (1), world (2), says (3), hel-lo (4), too (5). No, that's only 5. I need two more syllables. Maybe "The world hums in reply." That's He-world-hums-in-reply. Let me break it down: the (1), world (2), hums (3), in (4), re ply 
(5 and 6). Wait, no, that's only 6.

Wait, maybe I can use "The world answers back." The (1), world (2), an-sers (3), back (4). That's 4 syllables. Not enough. Hmm.

Alternatively, think about nature sounds. Maybe "Birds sing in reply." That's 7 syllables: Birds (2), sing (1), in (1), reply (2). Wait, that doesn't add up. Let me count each word as syllables:

"Birds" - 1
"sing" - 1
"in" -1
"reply" -2

Total: 5. Not enough.

Wait, perhaps "Birdsong answers back." That's 7 syllables: Bird-song (2), an-swers (2), back (1). Wait, no:

"Birdsong" is two syllables, "answers" is two, "back" is one. Total 5 again.

I'm getting stuck on the second line. Maybe I should think of something else. How about a sunrise? "Sunrise breaks the silence." That's 7 syllables: Sun-rise (2), breaks (1), the (1), si-lence (2). Wait, that adds up to 6 syllables.

Wait, no: Sunrise is two, breaks is one, the is one, silence is two. Total 6. Hmm.

Alternatively, "Gentle breeze returns." That's 7? Gentle (2), breeze (1), re-turns (2). Wait, gentle is two syllables, breeze is one, returns is two: total 5.

Hmm, maybe I'm overcomplicating. Let me try to think differently. Maybe the second line can be about reaching out or communication. "I reach out to you." That's 6 syllables: I (1), rea-ech (2), out (1), to (1), you (1). No, that's only 5.

Wait, maybe "I send a greeting far and wide." That's way too long. Let me think about natural elements again. Maybe water? "Waves whisper hello." That's waves (1), whisper (2), hel-lo (2). Wait: waves (1), whisper (2), hello (2). Total 5 syllables, but I need 7.

Alternatively, "Ripples greet the shore." Ripples (2), greet (1), the (1), shore (1). That's 5 again.

I'm getting a bit stuck. Let me look back at the first line: "Hello, gentle dawn." Maybe the second line can continue that imagery. Dawn brings light, so perhaps something like "Light breaks the silence." That's 6 syllables: Light (1), breaks (1), the (1), si-lence (2). Total 5 again.

Wait, maybe "Dawn greets the morning sky." That's dawn (1), greets (1), the (1), morn-ing (2), sky (1). Total is 6 syllables. Hmm.

Alternatively, "A new day starts with a smile." New (1), day (1), starts (1), with (1), a (1), smile (1). That's 6.

I'm still not hitting that 7-syllable mark for the second line. Maybe I should try something else. How about focusing on the feeling of greeting? "Heart beats in rhythm." Heart (1), beats (1), in (1), rhyth-m (2). Total is 5.

Wait, maybe "Echoes fill the air." Echoes (2), fill (1), the^C

As before, my next step was to see if the model could code a simple "Hello, world!" program in Scala. The 1.5B-parameter model failed abysmally. Even when presented with compiler errors, and then my suggestions to tutor it, it was not making progress towards a correct solution.

The 14B parameter produced a correct program on its first attempt. It's <think>...</think>-ing was not perfect, but was mostly pretty good.

>>> Please write a Scala program that prints "Hello, world!"
<think>
Okay, I need to write a Scala program that prints "Hello, world!". Hmm, where do I start? Well, I know that in Scala, you can print something using the println function. So maybe I just call println("Hello, world!")?

Wait, but how does aScala script work? Do I need to have an object or is it simpler than that? Oh right, for a simple program like this, I think you can just write it in a single line. But wait, when compiling with scalac, do I need to structure it as an object?

Let me think. If I'm writing a script, maybe I can use the main method inside an object. So perhaps the code should be something like:

object HelloWorld {
  def main(args: Array[String]): Unit = {
    println("Hello, world!")
  }
}

Yes, that makes sense. Because when you run a Scala program, it looks for an object with a main method. So this structure is correct.

Alternatively, I remember that in older versions of Scala, you could just write the code without defining an object if using the scalaz script compiler or something like that. But perhaps the more standard way is to define an object with the main method.

So putting it all together, the code should be an object named HelloWorld (or any name really), and inside the main function, we call println with the desired string.

Let me double-check: When I run this program, either by compiling with scalac and then running with scala, or using a Scala IDE, it should print "Hello, world!" correctly. Yeah, that should work.
</think>

To write a Scala program that prints "Hello, world!", follow these steps:

1. **Create a new file** for your Scala program. It can be named `HelloWorld.scala`.

2. **Write the code**: The code will define an object with a main method that calls `println` to output the desired message.

Here's the complete Scala code:

```scala
object HelloWorld {
  def main(args: Array[String]): Unit = {
    println("Hello, world!")
  }
}
``` 

3. **Run the program**:
   - **Using the command line**:
     - Compile the file using `scalac HelloWorld.scala`.
     - Run it with ` scala HelloWorld`.
   - **Using an IDE**: Simply run the script through your preferred Scala IDE.

This program will output:

``` 
Hello, world!
``` 

And so it did.

% scala-cli run hello-ds14b.scala 
Compiling project (Scala 3.6.3, JVM (17))
Compiled project (Scala 3.6.3, JVM (17))
Hello, world!

Since it got it right, I thanked it! Its "thought-process" in response was kind of fun:

>>> Great! Thank you.
<think>
Alright, so the user just thanked me for the previous explanation on writing a "Hello, world!" program in Scala. They were probably satisfied with the answer, but I should think about how to respond appropriately. 

I want to be polite and encouraging, so I'll acknowledge their gratitude. Maybe add an emoji to keep it friendly. Also, since they might be new to programming orScala, offering further help is a good idea. I can ask if there's anything else they need assistance with.

Hmm, perhaps I should also provide options for what they might want next—like solving another problem or explaining more concepts. That way, it shows I'm ready to assist them in various ways.

I should keep the response concise and positive, making sure they feel comfortable asking more questions if needed.
</think>

You're welcome! If you have any other questions or need help with anything else, feel free to ask! 😊

I really want to do whatever AI-using I eventually do on my own infrastructure, or infrastructure of someone I trust, rather than a distant megafirm. But this current family of models, on my current best hardware, won't get me there.

Sometime soon I may upgrade to an Apple Silicon Mac, and then retry these exercises.

I'd consider a cloud server I rent and fully control (as opposed to API access) "my own infrastructure", but cloud servers with the resources of my laptop are too pricey for me, for now. The trendy AI-ready GPU-endowed servers some cloud providers are renting are way beyond my price point.

So, for now, I think I'm stuck with the megafirms, to the degree that I use this stuff at all. But the DeepSeek models are a big step closer towards LLMs more consistent with our independence, and I'm grateful for that.

Baby DeepSeek on a Mac laptop


TL; DR: It's really fun to watch DeepSeek-R1-1.5B, <think>...</think>.

But this tiniest DeepSeek model is, um, not so smart. It seems to misinterpret the prompt input a fair amount, and draws inferences that are not quite coherent, even though in some superficial but nonsensical way you can understand the connections it's drawing.


For the heck of it, I decided to try to run the smallest DeepSeek generative AI model on my laptop. My laptop is an Intel MacBook Pro purchased in 2020 (but I think a 2019 model), 8 cores. It's tricked out with 64GB of memory.

After checking out a DigitalOcean tutorial for clues, Running the model was beyond trivial:

% brew install ollama
% brew services start ollama
% ollama run deepseek-r1:1.5b
pulling manifest 
pulling aabd4debf0c8... 100% ▕██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 1.1 GB                         
pulling 369ca498f347... 100% ▕██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏  387 B                         
pulling 6e4c38e1172f... 100% ▕██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 1.1 KB                         
pulling f4d24e9138dd... 100% ▕██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏  148 B                         
pulling a85fe2a2e58e... 100% ▕██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏  487 B                         
verifying sha256 digest 
writing manifest 
success 
>>> Send a message (/? for help)

That last line is prompting me for a prompt.

When I run prompts, I find that the model does not stress out my memory at all, but does use up 5 to 6 cores of CPU while it is active.

For the purpose of this post, I tried what I thought would be a simple prompt:

>>> Write a 'Hello, world!' haiku.

The model generates a <think>...</think> section in which it describes its own "thought" process. But it is so neurotic! Endearingly neurotic! It went around in circles. I eventually grew impatient and aborted it with <ctrl-c>.

<think>
Okay, so I need to write a haiku in lowercase for "Hello, world!" that reads backwards both in the traditional way and when reversed again. Let me start by recalling what a haiku is. It's a three-line poem where the first line has five characters, the second eight, and the third five, 
all in iambic pentameter.

First, I'll break down "Hello, world!". The words are H, E, L, L, O, ,, W, O, R, L, D. But since we're writing it in lowercase as "hello, world!", that's h e l l o , w o r l d. Now, for the haiku structure, I need to arrange these into three lines with specific syllable counts and stress 
patterns.

The first line should have five syllables. So I'll pick a part of "hello" that has five letters, counting each vowel as one syllable. H-e-l-ll-o is 1+1+2+2+1=7 syllables. Hmm, too many. Maybe h-e-llo would be better—h (1) + e (1) + l (1) + l (1) + o (1) = 5 syllables.

Wait, but "hello" is H E L L O, so five letters with four consonants and one vowel. So that's correct for the first line: h-e-l-l-o.

Now, I need to make it into a haiku. The second line should have eight syllables, and the third line back to five again. Also, when reversed in both ways, they should form "hello, world!" correctly.

Let me think about how to mirror "hello" in some way. Maybe using symmetrical patterns or mirrors. Alternatively, considering the punctuation: comma and exclamation mark. The second word is a bit tricky with the comma—perhaps that's intentional as part of the haiku structure.

Looking at "hello", the next word after the first line would be two syllables for eight in total, but including the comma it might need to be longer. Maybe h-e-ll-o-, which has four letters plus the comma. That could work: h-e-l-l-o- (four characters) and then another line.

Wait, let me try that. First line: h-e-l-l-o—5 syllables. Second line: h-e-llo- (but wait, h-e-l-l-o is five letters; maybe I'm overcomplicating). Alternatively, the second word could be "ll" with a hyphen or something? But in haikus, usually words are separated by spaces.

Wait, perhaps it's better to think of the syllables. The first line: 1+1+2+2+1=7 syllables—need to adjust to 5. So maybe h-e-l-l-o is correct as five letters, but I need to represent it with five syllables in a four-letter base? Or perhaps use an alliteration.

Alternatively, think about "hello" and mirror the words somehow. Maybe H-e-llo-world? But that's eight letters for two lines and five again. Wait, no—haiku is three lines: 5-8-5.

So first line: h-e-l-l-o (h + e + l + l + o = 5 syllables). Second line needs to have eight syllables. Maybe h-e-ll-o-w (but that's only seven letters; maybe with an exclamation or hyphen? Alternatively, think of the second word as a two-syllable word but longer in terms of letters.

Wait, perhaps I'm approaching this wrong. Let me try to mirror "hello" around the center. So H E L L O would be mirrored as D O L L E, but that doesn't fit into the haiku structure. Alternatively, think of it in a way where each word relates somehow.

Alternatively, consider that the second line is a contraction or reflection. Maybe h-e-ll-o-, then w-o-r-l-d. But I'm not sure about the syllables here. Let me count:

First line: h-e-l-l-o → 5 syllables.
Second line: h-e-llo-w (h + e + l + l + o + w → six letters, which is more than eight; maybe this approach isn't right.)

Alternatively, perhaps using alliteration or a more specific pattern. Let me think of "hello" and how it can be mirrored with the second word being two syllables but having more letters.

Wait, another approach: The first line is 5 syllables (h-e-l-l-o), then for eight, maybe h-e-ll-o-w (but that's six characters; perhaps including a hyphen or something). Alternatively, think of "hello" and "world" as related in some way, like the second word being two syllables but 
longer.

Wait, let me consider "h-e-llo-world"—but no, it would be 12 letters. Maybe h-e-ll-o-w (but I'm getting confused with syllable counts).

Alternatively, perhaps think of each word as having a certain number of stressed vowels or consonants. For example, in the second line, the stress pattern might require two stressed syllables.

Wait, maybe it's better to look for existing haikus and see if any are similar to "hello" that can be mirrored. But since I'm supposed to create this from scratch, let me try again.

First line: h-e-l-l-o → 5 syllables.
Second line needs to have eight syllables. Maybe h-e-llo-w (h + e + l + l + o + w = six letters) but that's seven characters with an exclamation mark—maybe h-e-ll-o-w? Let me check the stress pattern for "hello". The word is h-e-l-l-o, which has four consonants and one vowel. So it 
sounds like a single syllable maybe.

Wait, no: 'Hello' has two syllables: H-e and llo, but in reality, it's pronounced as one syllable. But I need to make the haiku with five syllables for the first line.

Alternatively, perhaps h-e-ll-o would have four consonants, so that's 4 letters which is too few for a five-syllable word. Wait, no: h (1) + e (2) + l (3) + l (4) + o (5). So it's five syllables.

So first line: H E L L O → 5 syllables.

Second line needs to have eight syllables. Maybe h-e-ll-o-w? Let me check the syllable count for "hello, world": two lines of 5 and 8 respectively. Wait, no—the haiku is five, then eight, then five. But in this case, first line: hello (h-e-l-l-o) → five syllables.

Second line: h-e-ll-o-w? Let's see: h (1), e (2), l (3), l (4), o (5), w (6). That's six letters, which would be eight syllables if each word is broken down. Wait, that doesn't add up.

Alternatively, perhaps "h-e-ll-o-w" as the second line but considering more syllables. Hmm, I'm getting stuck here. Maybe I should look for existing haikus in the style of "hello, world!" and see how they mirror each other.

Wait, another idea: The second word is two syllables, so maybe h-e-ll-o-w? That's six letters but seven characters (including the comma). Alternatively, perhaps it's h-e-llo-w with a hyphen or something. But I'm not sure.

Alternatively, think of "h-e-ll-o" as one line and then mirroring with "w-o-r-l-d" in reverse. So maybe h-e-ll-o-, w-o-rld? Let me check the syllables:

First line: h-e-l-l-o → five syllables.
Second line: h-e-ll-o-w (h + e + l + l + o + w = six letters) but that's seven characters with a comma. Maybe hyphenated or something else, but it's tricky.

Alternatively, perhaps the second word is "h-e-llo", which is four syllables, and then "world" as two? No, that doesn't fit.

Wait, maybe I'm overcomplicating this. Let me try to write the lines step by step:

First line: h-e-l-l-o → 5 syllables.
Second line needs to be eight syllables. Maybe h-e-ll-o-w (six letters) but with a hyphen or something, making it seven characters. Alternatively, think of "h-e-ll-o-w" as the second word, which would make the entire line longer.

Alternatively, maybe I'm supposed to mirror the words somehow. So H-E-L-L-O could be mirrored around the center, perhaps h-e-llo-world, but that's too long.

Wait, another idea: The first line is 5 syllables, and the second word is two syllables with more letters. For example, h-e-ll-o (five letters) vs. w-o-r-l-d (but that would be in reverse). Alternatively, think of "h-e-ll-o" as one line and then "world" mirrored.

Wait, perhaps I'm stuck. Let me try to write the second line as h-e-ll-o-w with a hyphen or something, making it 6 characters. But no, haikus have strict syllable counts.

Alternatively, think of each word in terms of syllables: first line is five, second is eight, third is five.

So second line needs to be eight syllables. Maybe h-e-llo-w (h + e + l + l + o + w = six letters) but that's seven characters. Alternatively, maybe "h-e-ll-o" and "w-d", making it two lines: h-e-ll-o- and w-o-rld? No, that doesn't make sense.

Wait, I'm getting stuck here. Let me try to write the second line as h-e-llo-w with a hyphen in the middle or something, but^C

>>>

Then I tried a simpler variation:

>>> Write me a joyful greeting, please?
<think>
Okay, so I just received this message: "Write me a joyful greeting, please?" Hmm, that's interesting. At first glance, it sounds like someone is trying to ask for a greeting but maybe they don't realize how important it is. Let me try to unpack what's going on here.

So, the person wrote a greeting that ends with a question mark. That's a bit unusual because usually, greetings are polite and direct without any questions. Maybe they're testing something or just curious about why they're asking for this specific type of greeting. Perhaps they want to 
see if I can recognize different types of requests.

I should consider what kind of response they might expect. Since it ends with a question mark, maybe the next step is to acknowledge their request and provide a friendly reply. But how? A simple "Yes! Thank you for your kindness!" would be appropriate. It's concise and acknowledges their 
message without being repetitive or too formal.

I wonder if there are any underlying reasons behind their query. Maybe they're trying to get my attention quickly or set a context where I can respond further. Without more information, it's hard to say exactly what they need next, but focusing on a heartfelt greeting seems like the best 
approach right now.

Also, considering the tone, it should feel warm and caring. Using words that convey gratitude and appreciation is key here. Phrases like "I appreciate your kind words" or "Thank you for your kind gesture" can make the response more genuine.

Another angle to think about is the purpose of the greeting. If this is part of a larger conversation, maybe they need something specific for that context. But since I don't have any additional details, it's safer to keep it general and flexible.

I should also consider if there are alternative greetings that might be more suitable based on their message. For example, "How can I help you today?" or "Could you please let me know how I can assist you?" Each of these has its own merits, but the original question ends with a question 
mark, so focusing back on a simple, heartfelt response makes sense.

In summary, my approach should be to acknowledge their message, provide a genuine reply that shows appreciation, and keep it concise. This way, they know I'm listening without overcomplicating things.
</think>

Certainly! Here's a heartfelt greeting in the requested format:

---

Hey there! I hope you're doing well today. It sounds like your kind words are always appreciated—thanks for taking the time to connect with me!

---

This response is warm, genuine, and acknowledges their message while keeping it concise.

A bit off. But not terrible!

Next I tried to see if it could code:

>>> Please write a Scala program that prints "Hello, world!"

This... didn't go so well.

To be fair, Scala is not likely to be its most prominant training language, and some of its attempts could be excused as not quite knowing whether to write Scala 2 or Scala 3. Its first attempt was closer to Scala 3 than Scala 2. (It offered a naked top-level function, supported as main entry points only in Scala 3.) So I tried to tutor it towards s Scala 3 solution.

It came pretty close! But after three or four back and forths over ~25 minutes, it still wasn't quite right and I gave up.

This is the tiniest DeepSeek model. I wonder if I can run a bigger one, and if it performs a bit better?

Stay tuned on that, I guess.


Postscript:

I'm pretty short storage space on the ol' laptop, so I want to clean up after this exercise. A bit of searching tells me that on a Mac, models will be stored under a ~/.ollama directory.

% find ~/.ollama 
/Users/swaldman/.ollama
/Users/swaldman/.ollama/id_ed25519
/Users/swaldman/.ollama/id_ed25519.pub
/Users/swaldman/.ollama/models
/Users/swaldman/.ollama/models/blobs
/Users/swaldman/.ollama/models/blobs/sha256-6e4c38e1172f42fdbff13edf9a7a017679fb82b0fde415a3e8b3c31c6ed4a4e4
/Users/swaldman/.ollama/models/blobs/sha256-369ca498f347f710d068cbb38bf0b8692dd3fa30f30ca2ff755e211c94768150
/Users/swaldman/.ollama/models/blobs/sha256-aabd4debf0c8f08881923f2c25fc0fdeed24435271c2b3e92c4af36704040dbc
/Users/swaldman/.ollama/models/blobs/sha256-a85fe2a2e58e2426116d3686dfdc1a6ea58640c1e684069976aa730be6c1fa01
/Users/swaldman/.ollama/models/blobs/sha256-f4d24e9138dd4603380add165d2b0d970bef471fac194b436ebd50e6147c6588
/Users/swaldman/.ollama/models/manifests
/Users/swaldman/.ollama/models/manifests/registry.ollama.ai
/Users/swaldman/.ollama/models/manifests/registry.ollama.ai/library
/Users/swaldman/.ollama/models/manifests/registry.ollama.ai/library/deepseek-r1
/Users/swaldman/.ollama/models/manifests/registry.ollama.ai/library/deepseek-r1/1.5b
/Users/swaldman/.ollama/history
% du -sh ~/.ollama 
1.0G	/Users/swaldman/.ollama

It's not huge, but I'm going to remove it anyway for now.

% ollama help
Large language model runner

Usage:
  ollama [flags]
  ollama [command]

Available Commands:
  serve       Start ollama
  create      Create a model from a Modelfile
  show        Show information for a model
  run         Run a model
  stop        Stop a running model
  pull        Pull a model from a registry
  push        Push a model to a registry
  list        List models
  ps          List running models
  cp          Copy a model
  rm          Remove a model
  help        Help about any command

Flags:
  -h, --help      help for ollama
  -v, --version   Show version information

Use "ollama [command] --help" for more information about a command.

% ollama list
NAME                ID              SIZE      MODIFIED
deepseek-r1:1.5b    a42b25d8c10a    1.1 GB    About an hour ago

% ollama rm deepseek-r1:1.5b
deleted 'deepseek-r1:1.5b'

% du -sh ~/.ollama
 12K	/Users/swaldman/.ollama

All cleaned up!

2025-01-14

Syndicating RSS to Mastodon and BlueSky with feedletter


feedletter is a service I wrote mostly to convert RSS newsfeeds into e-mail newsletters.

If you subscribe to this blog by e-mail, or to drafts.interfluidity.com, feedletter is the service to which you subscribed, and which mails you my blogposts.

I wrote an elaborate tutorial documenting how to set up newsletter subscriptions about a year ago.

But feedletter can also syndicate RSS announcements (or posts, length permitting) to Mastodon and BlueSky.

I thought I'd do a quick tutorial on how it works.

Table of contents

  1. Prerequisites
  2. Create user feedletter
  3. Install feedletter
  4. Prepare the postgres database
  5. Create the /etc/feedletter directory
  6. Create a Mastodon app password
  7. Create /etc/feedletter/feedletter-secrets.properties and configure Mastodon access
  8. Configure BlueSky access
  9. Add c3p0 (database) configuration to /etc/feedletter/feedletter-secrets.properties
  10. Add a "secret salt" to /etc/feedletter/feedletter-secrets.properties
  11. Check out the feedletter app
  12. Initialize the feedletter database
  13. Add a feed you wish to syndicate
  14. Define your subscribables
  15. Subscribe the target Mastodon and BlueSky accounts to the subscribables.
  16. Test in the terminal
  17. Enable feedletter as a systemd daemon
  18. Conclusion

1. Prerequisites

You'll need a systemd-based server on the internet, git, postgres, and a Java 17+ JVM installed.

2. Create user feedletter

We'll create a passwordless user:

# adduser --disabled-password feedletter

3. Install feedletter

We just clone a git repository. We'll make it become /opt/feedletter, and make sure our new user feedletter owns it:

# cd /opt
# git clone https://github.com/swaldman/feedletter-install.git feedletter
Cloning into 'feedletter'...
remote: Enumerating objects: 81, done.
remote: Counting objects: 100% (81/81), done.
remote: Compressing objects: 100% (48/48), done.
remote: Total 81 (delta 40), reused 64 (delta 23), pack-reused 0 (from 0)
Receiving objects: 100% (81/81), 12.32 KiB | 4.10 MiB/s, done.
Resolving deltas: 100% (40/40), done.
# chown -R feedletter:feedletter feedletter

4. Prepare the postgres database

# su - postgres
$ createdb feedletter
$ createuser feedletter
$ psql
psql (16.6 (Ubuntu 16.6-0ubuntu0.24.10.1))
Type "help" for help.

postgres=# ALTER DATABASE feedletter OWNER TO feedletter;
ALTER DATABASE
postgres=# ALTER USER feedletter WITH PASSWORD 'not-actually-this';
ALTER ROLE
postgres=# \q
$ exit

5. Create the /etc/feedletter directory

# mkdir /etc/feedletter
# chown feedletter:feedletter /etc/feedletter

6. Create a Mastodon app password, and place it in /etc/feedletter/feedletter-secrets.properties

In the Mastodon account to which you intend to syndicate announcements, go to Settings > Development, and click the "New Application" button.

Define an application name. We'll use "feedletter-syndication".

Be sure to enable write permissions!

Finally click the Submit button.

You should see your new application listed now in the applications page. Click on it. On the resulting page, note the field "Your access token". You'll need its value in the next step.

7. Create /etc/feedletter/feedletter-secrets.properties and configure Mastodon access

We'll first become user feedletter, then open a text editor on /etc/feedletter/feedletter-secrets.properties.

# su - feedletter
$ touch /etc/feedletter/feedletter-secrets.properties
$ chmod 600 /etc/feedletter/feedletter-secrets.properties
$ emacs /etc/feedletter/feedletter-secrets.properties

feedletter will insist that /etc/feedletter/feedletter-secrets.properties has restrictive permissions.

It will refuse to run if you skip the chmod bit.

$ chmod 600 /etc/feedletter/feedletter-secrets.properties`

Now we'll add an entry to our properties file in the form

feedletter.masto.access.token.<arbitrary-name>=<access-token>`:

Just to be dumb, we'll use "arbitrary-name" as our, um, arbitrary name.

feedletter.masto.access.token.arbitrary-name=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Your access token probably is not all XXXXXXXXXX!

8. Configure BlueSky access

In the BlueSky account you intend to syndicate to, go to Settings > Privacy and security > App passwords.

Click "Add App Password".

Give your app a name. We'll use "feedletter-syndication" again.

Your app password will appear.

Add a new line to /etc/feedletter/feedletter-secrets.properties, in the form feedletter.bsky.identifier.<identifier>=<app-password>.

This is a bit different from Mastodon!

In Mastodon, a totally arbitrary name went at the end of the key. Here it is the user identifier, which usually a DNS handle (but it can also be a atproto did).

The account we're syndicating to will be testable-bsky.bsky.social

So /etc/feedletter/feedletter-secrets.properties should look something like this, albeit with a real access tokens / passwords.

feedletter.masto.access.token.arbitrary-name=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
feedletter.bsky.identifier.testable-bsky.bsky.social=xxxx-xxxx-xxxx-xxxx

9. Add c3p0 (database) configuration to /etc/feedletter/feedletter-secrets.properties

Remember when we set the Postgres password a while back? We'll use it now.

Postgres by default runs on port 5432, so let's add some database access and authentication information.

feedletter uses the c3p0 connection pooling library.

Edit /etc/feedletter/feedletter-secrets.properties so that it looks something like this:

feedletter.masto.access.token.arbitrary-name=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
feedletter.bsky.identifier.testable-bsky.bsky.social=xxxx-xxxx-xxxx-xxxx
c3p0.jdbcUrl=jdbc:postgresql://localhost:5432/feedletter
c3p0.user=feedletter
c3p0.password=not-actually-this

10. Add a "secret salt" to /etc/feedletter/feedletter-secrets.properties

/etc/feedletter/feedletter-secrets.properties should contain a "secret salt", whose value is entirely arbitrary but should be kept secret. We'll add that, so /etc/feedletter/feedletter-secrets.properties now looks like:

feedletter.masto.access.token.arbitrary-name=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
feedletter.bsky.identifier.testable-bsky.bsky.social=xxxx-xxxx-xxxx-xxxx
c3p0.jdbcUrl=jdbc:postgresql://localhost:5432/feedletter
c3p0.user=feedletter
c3p0.password=not-actually-this
feedletter.secret.salt=This is an arbitrary string.

11. Check out the feedletter app

As user feedletter, cd into /opt/feedletter and type

$ ./feedletter --help

The first time you do this, it will download a lot of stuff!

Eventually you will see a help message, like this:

Usage:
     feedletter [--secrets <propsfile>] add-feed
     feedletter [--secrets <propsfile>] alter-feed
     feedletter [--secrets <propsfile>] daemon
     feedletter [--secrets <propsfile>] db-dump
     feedletter [--secrets <propsfile>] db-init
     feedletter [--secrets <propsfile>] db-migrate
     feedletter [--secrets <propsfile>] define-bluesky-subscribable
     feedletter [--secrets <propsfile>] define-email-subscribable
     feedletter [--secrets <propsfile>] define-mastodon-subscribable
     feedletter [--secrets <propsfile>] drop-feed-and-subscribables
     feedletter [--secrets <propsfile>] drop-subscribable
     feedletter [--secrets <propsfile>] edit-subscribable
     feedletter [--secrets <propsfile>] export-subscribers
     feedletter [--secrets <propsfile>] list-config
     feedletter [--secrets <propsfile>] list-feeds
     feedletter [--secrets <propsfile>] list-items-excluded
     feedletter [--secrets <propsfile>] list-subscribables
     feedletter [--secrets <propsfile>] list-subscribers
     feedletter [--secrets <propsfile>] list-untemplates
     feedletter [--secrets <propsfile>] send-test-email
     feedletter [--secrets <propsfile>] set-config
     feedletter [--secrets <propsfile>] set-extra-params
     feedletter [--secrets <propsfile>] set-untemplates
     feedletter [--secrets <propsfile>] subscribe
 
 Manage e-mail subscriptions to and notifications from RSS feeds.
 
 Options and flags:
     --help
         Display this help text.
     --secrets <propsfile>
         Path to properties file containing SMTP, postgres, c3p0, and other configuration details.
 
 Environment Variables:
     FEEDLETTER_SECRETS=<path>
         Path to properties file containing SMTP, postgres, c3p0, and other configuration details.
 
 Subcommands:
     add-feed
         Add a new feed from which mail or notifications may be generated.
     alter-feed
         Alter the timings of an already-defined feed.
     daemon
         Run daemon that watches feeds and sends notifications.
     db-dump
         Dump a backup of the database into a configured directory.
     db-init
         Initialize the database schema.
     db-migrate
         Migrate to the latest version of the database schema.
     define-bluesky-subscribable
         Define a BlueSky subscribable, a source from which BlueSky feeds can receive automatic posts..
     define-email-subscribable
         Define a new email subscribable, a mailing lost to which users can subscribe.
     define-mastodon-subscribable
         Define a Mastodon subscribable, a source from which Mastodon feeds can receive automatic posts..
     drop-feed-and-subscribables
         Removes a feed, along with any subscribables defined upon it, from the service.
     drop-subscribable
         Removes a subscribable from the service.
     edit-subscribable
         Edit an already-defined subscribable.
     export-subscribers
         Dump subscriber information for a subscribable in CSV format.
     list-config
         List all configuration parameters.
     list-feeds
         List all feeds the application is watching.
     list-items-excluded
         List items excluded from generating notifications.
     list-subscribables
         List all subscribables.
     list-subscribers
         List all subscribers to a subscribable.
     list-untemplates
         List available untemplates.
     send-test-email
         Send a brief email to test your SMTP configuration.
     set-config
         Set configuration parameters.
     set-extra-params
         Add, update, or remove extra params you may define to affect rendering of notifications and messages.
     set-untemplates
         Update the untemplates used to render subscriptions.
     subscribe
         Subscribe to a subscribable.

This might be surrounded by some noise from the mill process used to launch it. It might end with Subprocess failed.

That's all fine.

12. Initialize the feedletter database

As user feedletter, from within /opt/feedletter, type

$ ./feedletter db-init

That's all!

13. Add a feed you wish to syndicate

As user feedletter, from within /opt/feedletter, type

$ ./feedletter add-feed --help

You should see

 Usage:
     feedletter add-feed --ping <feed-url>
     feedletter add-feed [--min-delay-minutes <minutes>] [--await-stabilization-minutes <minutes>] [--max-delay-minutes <minutes>] [--recheck-every-minutes <minutes>] <feed-url>
 
 Add a new feed from which mail or notifications may be generated.
 
 Options and flags:
     --help
         Display this help text.
     --ping
         Check feed as often as possible, notify as soon as possible, regardless of (in)stability.
     --min-delay-minutes <minutes>
         Minimum wait (in miunutes) before a newly encountered item can be notified.
     --await-stabilization-minutes <minutes>
         Period (in minutes) over which an item should not have changed before it is considered stable and can be notified.
     --max-delay-minutes <minutes>
         Notwithstanding other settings, maximum period past which an item should be notified, regardless of its stability.
     --recheck-every-minutes <minutes>
         Delay between refreshes of feeds, and redetermining items' availability for notification.

feedletter is general very cautious about announcing / e-mailing posts. Since posts are often edited soon after they are published, by default it waits a period of time, and then checks to make sure the post has been stable a while before announcing or e-mailing. By default min-delay-minutes is 30, await-stabilization-minutes is 15, max-delay-minutes is 180, and recheck-every-minutes is 10.

It's probably best to use cautious settings like this, so you don't announce your links while you are still re-editing them. (I don't think I'm the only writer who inevitably finds important edits just after hitting publish.)

For this tutorial, I'm going to use a feed of all the articles in my newsreader, inoreader. Its URL looks like

https://www.inoreader.com/stream/user/0000000000/tag/all-articles,

although 0000000000 is not in fact my user id, I'm altering that for privacy. You should use whatever feed you are interested in syndicating.

To use the defaults, you could just type

$ ./feedletter add-feed https://www.inoreader.com/stream/user/0000000000/tag/all-articles

For the purposes of our little experiment here, I want articles announced as soon as feedletter sees them, even if they have not "stabilized". For that, it will just be

$ ./feedletter add-feed --ping https://www.inoreader.com/stream/user/0000000000/tag/all-articles

You should see something like

+---------+-------------------------------------------------------------------+----------------+--------------------------+----------------+--------------------+-----------------------------+-----------------------------+
¦ Feed ID ¦ Feed URL                                                          ¦ Min Delay Mins ¦ Await Stabilization Mins ¦ Max Delay Mins ¦ Recheck Every Mins ¦ Added                       ¦ Last Assigned               ¦
+---------+-------------------------------------------------------------------+----------------+--------------------------+----------------+--------------------+-----------------------------+-----------------------------+
¦ 1       ¦ https://www.inoreader.com/stream/user/0000000000/tag/all-articles ¦ 0              ¦ 0                        ¦ 0              ¦ 0                  ¦ 2025-01-11T05:00:47.128363Z ¦ 2025-01-11T05:00:47.128363Z ¦
+---------+-------------------------------------------------------------------+----------------+--------------------------+----------------+--------------------+-----------------------------+-----------------------------+

The numbers will be different if you (wisely!) don't use the --ping flag.

Using --ping will cause the feed to be checked very frequently, currently about once a minute. Some servers may grow cross! You may be blocked or rate-limited!

You can use ./feedletter alter-feed to set a gentler recheck-every-minutes.

14. Define your subscribables

feedletter is a layered application. First you tell it what feeds to watch, then you define different kinds of "subscribables" — e.g. email, Mastodon, BlueSky — to each of which multiple recipients can subscribe in order to receive notifications.

Typically an e-mail subscribable will have many — 100s, 1000s — of destinations.

BlueSky and Mastodon "subscribable" often have just one subscriber, the accounts to which entries will be syndicated. You can attach as many subscribers as you want, though, if you want to post announcements to many accounts.

Let's start with our Mastodon subscribable:

$ ./feedletter define-mastodon-subscribable --help

You'll see something like

 Usage: feedletter define-mastodon-subscribable --feed-id <feed-id> --name <name> [--extra-param <key:value>]...
 
 Define a Mastodon subscribable, a source from which Mastodon feeds can receive automatic posts..
 
 Options and flags:
     --help
         Display this help text.
     --feed-id <feed-id>
         The ID of the RSS feed to be watched.
     --name <name>
         A name for the new subscribable.
     --extra-param <key:value>
         An extra parameter your notification renderers might use.

We saw the feed-id in the output to ./feedletter add-feed (and if we forget it, we can always run ./feedletter list-feeds).

The subscribable name can be anything we want, but by default it will appear in announcements. I'm going to call this subscribable stevefeeds-masto:

$ ./feedletter define-mastodon-subscribable --feed-id=1 --name=stevefeeds-masto
 
 -*-*-*-
 
 Subscribable Name:    stevefeeds-masto
 Feed ID:              1
 Subscription Manager: {
     "extraParams": {},
     "type": "Mastodon.Announce",
     "version": 1
 }
 A Mastodon subscribable to feed with ID '1' named 'stevefeeds-masto' has been created.

Let's do the same, and make a BlueSky subscribable:

$ ./feedletter define-bluesky-subscribable --feed-id=1 --name=stevefeeds-bsky

 -*-*-*-
 
 Subscribable Name:    stevefeeds-bsky
 Feed ID:              1
 Subscription Manager: {
     "extraParams": {},
     "type": "BlueSky.Announce",
     "version": 1
 }
 A BlueSky subscribable to feed with ID '1' named 'stevefeeds-bsky' has been created.

15. Subscribe the target Mastodon and BlueSky accounts to the subscribables.

Let's check out the subscribe subcommand.

$ ./feedletter subscribe --help

 Usage:
     feedletter subscribe --subscribable-name <name> --e-mail <address> [--display-name <name>] [--unconfirmed]
     feedletter subscribe --subscribable-name <name> --sms <number> [--unconfirmed]
     feedletter subscribe --subscribable-name <name> --masto-instance-name <name> --masto-instance-url <url> [--unconfirmed]
     feedletter subscribe --subscribable-name <name> --bsky-identifier <identifier> [--bsky-entryway-url <name>] [--unconfirmed]
 
 Subscribe to a subscribable.
 
 Options and flags:
     --help
         Display this help text.
     --subscribable-name <name>
         The name of an already-defined subscribable.
     --e-mail <address>
         The e-mail address to subscribe.
     --display-name <name>
         A display name to wrap around the e-mail address.
     --sms <number>
         The number to which messages should be sent.
     --masto-instance-name <name>
         A private name for this Mastodon instance.
     --masto-instance-url <url>
         The URL of the Mastodon instance
     --bsky-identifier <identifier>
         An account identifier, usually a DNS name or value beginning with 'did:'.
     --bsky-entryway-url <name>
         The base URL of the bluesky service.
     --unconfirmed
         Mark the subscription unconfirmed.

Recall that when we placed our Mastodon access token in feedletter-secrets.properties, we gave it a name. We chose "arbitrary-name", because it's lovely. That becomes our masto-instance-name. The URL of the instance that hosts our accounts will be masto-instance-url. So we just...

./feedletter subscribe --subscribable-name stevefeeds-masto --masto-instance-name arbitrary-name --masto-instance-url https://mas.to/

For BlueSky, at least for now, we don't really need to provide a URL, there's just one. (In the future, hopefully there will be more BlueSky-like atproto services, for which you would provide and entryway-url). All you need to provide is bsky-identifier, the DNS handle or did associated with your BlueSky account.

./feedletter subscribe --subscribable-name stevefeeds-bsky --bsky-identifier testable-bsky.bsky.social

16. Test in the terminal

Finally, all of our setup is done!

We can get feedletter checking out our feed and syndicating announcements just by running what's usually the daemon process in the terminal.

$ ./feedletter daemon

Jan 13, 2025 5:33:34 PM com.mchange.v2.log.MLog 
INFO: MLog clients using java 1.4+ standard logging.
2025-01-13@17:33:36 [INFO] [com.mchange.feedletter.Daemon] feedletter-0.1.0-13-60e19c daemon (re)starting. 
2025-01-13@17:33:36 [INFO] [com.mchange.feedletter.Daemon] Spawning daemon fibers. 
2025-01-13@17:33:37 [INFO] [com.mchange.feedletter.Daemon] Starting web API service on interface '127.0.0.1', port 8024. 
timestamp=2025-01-13T17:33:37.596959114Z level=INFO thread=#zio-fiber-1822226262 message="Starting the server..." location=com.mchange.feedletter.Daemon.webDaemon file=Daemon.scala line=189
timestamp=2025-01-13T17:33:37.714089350Z level=INFO thread=#zio-fiber-1822226262 message="Server started" location=com.mchange.feedletter.Daemon.webDaemon file=Daemon.scala line=189
2025-01-13@17:33:50 [FINE] [com.mchange.feedletter.db.PgDatabase] Deleting any as-yet-unassigned items that have been deleted from feed with ID 1 
2025-01-13@17:33:50 [INFO] [com.mchange.feedletter.db.PgDatabase] Updated/assigned all items from feed with ID 1, feed URL 'https://www.inoreader.com/stream/user/0000000000/tag/all-articles' 
2025-01-13@17:33:59 [FINE] [com.mchange.feedletter.db.PgDatabase] Deleting any as-yet-unassigned items that have been deleted from feed with ID 1 
2025-01-13@17:33:59 [INFO] [com.mchange.feedletter.db.PgDatabase] Updated/assigned all items from feed with ID 1, feed URL 'https://www.inoreader.com/stream/user/0000000000/tag/all-articles' 
2025-01-13@17:34:03 [FINE] [com.mchange.feedletter.db.PgDatabase] Deleting any as-yet-unassigned items that have been deleted from feed with ID 1 

Great! This is what it looks like when the daemon is watch feeds. (In this case, just one feed.)

Eventually, when a new item is posted, you'll see entries like

2025-01-13@17:34:42 [FINE] [com.mchange.feedletter.db.PgDatabase] Added new item, feed ID 1, guid 'http://www.inoreader.com/article/3a9c6e7789e019a2'. 
2025-01-13@17:34:42 [FINE] [com.mchange.feedletter.db.PgDatabase] Item with GUID 'http://www.inoreader.com/article/3a9c6e7789e019a2' from feed with ID 1 has been assigned in subscribable 'stevefeeds-bsky' with assignable identifier 'http://www.inoreader.com/article/3a9c6e7789e019a2'. 
2025-01-13@17:34:42 [FINE] [com.mchange.feedletter.db.PgDatabase] Item with GUID 'http://www.inoreader.com/article/3a9c6e7789e019a2' from feed with ID 1 has been assigned in subscribable 'stevefeeds-masto' with assignable identifier 'http://www.inoreader.com/article/3a9c6e7789e019a2'. 
2025-01-13@17:34:42 [FINE] [com.mchange.feedletter.db.PgDatabase] Item with GUID 'http://www.inoreader.com/article/3a9c6e7789e019a2' from feed with ID 1 has been assigned (or refused assignment) in all subscribables to that feed. 

In English, a new item has been found, and it's been "assigned" to the various subscribables watching the feed, here one that distributes to mastodon, and another that distributes to Mastodon and another that distributes to BlueSky. (You can also make various forms of e-mail subscribables!)

Some subscribables collect many posts before distributing them. (For example, a weekly digest e-mail.)

But for Mastodon and BlueSky post announcements, each post should be distributed immediately following its observation. So soon after we see an add message, we'll see messages like...

2025-01-13@17:34:42 [FINE] [com.mchange.feedletter.db.PgDatabase] Queued Mastodon post for distribution. Content: [stevefeeds-masto] New Post: Thousands sign petition calling on ad titan WPP to rethink its 4-day RTO demand, by pthompson@insider.com (Polly Thompson) https://www.businessinsider.com/wpp-return-to-office-thousands-sign-petition-workers-2025-1 
2025-01-13@17:34:42 [FINE] [com.mchange.feedletter.db.PgDatabase] Assignable (item collection) defined by subscribable name 'stevefeeds-masto', within-type-id 'http://www.inoreader.com/article/3a9c6e7789e019b1' has been deleted. 
2025-01-13@17:34:42 [FINE] [com.mchange.feedletter.db.PgDatabase] Cached values of items fully distributed have been cleared. 
2025-01-13@17:34:42 [INFO] [com.mchange.feedletter.db.PgDatabase] Completed assignable 'http://www.inoreader.com/article/3a9c6e7789e019b1' with subscribable 'stevefeeds-masto'. 
2025-01-13@17:34:42 [INFO] [com.mchange.feedletter.db.PgDatabase] Cleaned away data associated with completed assignable 'http://www.inoreader.com/article/3a9c6e7789e019b1' in subscribable 'stevefeeds-masto'. 

After "completion", all data about the post is deleted, except the RSS guid and link are retained to prevent reannouncing the same post should it reappear in the RSS.

Once queued for distribution, you will see messages about postings to Mastodon and BlueSky.

Since on very active feeds, you can get a lot of posts at once when they are republished, feedletter spaces postings to a single account by one minute (currently hardcoded, eventually configurable), though it sends its posting streams to all subscribed accounts in parallel.

So you'll see stuff like this:

2025-01-13@17:52:48 [INFO] [com.mchange.feedletter.db.PgDatabase] Posted BlueSky notification to (https://bsky.social/,testable-bsky.bsky.social) 
2025-01-13@17:53:13 [FINE] [com.mchange.feedletter.db.PgDatabase] Deleting any as-yet-unassigned items that have been deleted from feed with ID 1 
2025-01-13@17:53:13 [INFO] [com.mchange.feedletter.db.PgDatabase] Updated/assigned all items from feed with ID 1, feed URL 'https://www.inoreader.com/stream/user/0000000000/tag/all-articles' 
2025-01-13@17:53:15 [INFO] [com.mchange.feedletter.db.PgDatabase] Posted Mastodon notification to (https://mas.to/,arbitrary-name) 

And we see posts!

Sometimes things do go wrong!

2025-01-13@17:56:49 [WARNING] [com.mchange.feedletter.db.PgDatabase] Failed attempt to post to Bluesky destination 'https://bsky.social/', retried = 0 (maxRetries: 10) 
requests.RequestFailedException: Request to https://bsky.social/xrpc/com.atproto.repo.createRecord failed with status code 400
{"error":"InvalidRequest","message":"Invalid app.bsky.feed.post record: Record/text must not be longer than 300 graphemes"}
	at requests.Requester$$anon$1.readBytesThrough(Requester.scala:360)
	at geny.Readable.writeBytesTo(Writable.scala:93)
	at geny.Readable.writeBytesTo$(Writable.scala:91)
        ...
2025-01-13@17:56:49 [WARNING] [com.mchange.feedletter.db.PgDatabase] Attempt to post BlueSky notification to (https://bsky.social/,testable-bsky.bsky.social) failed or was skipped. Postable: BskyPostable(6,[stevefeeds-bsky] New Post: In DISSENT's winter issue, I make the case for left-YIMBYism in a debate with Brian Callaci and Sandeep Vaheesan https://www.dissentmagazine.org/article/supply-and-the-housing-crisis-a-debate/, by @resnikoff.bsky.social - Ned Resnikoff https://bsky.app/profile/resnikoff.bsky.social/post/3lfnb24zzbc2c,https://bsky.social/,testable-bsky.bsky.social,0,List()) 

feedletter will retry failed postings up to 10 times by default (the retry count is reconfigurable), then give up. This issue — the announcement is just too long to fit in BlueSky's 300 character limit — will not be cured, so that's what will happen. (I'll probably modify the BlueSky subscribable to just reject too-long posts, but for now they are just tried, retried, and eventually dropped.)

17. Enable feedletter as a systemd daemon

We just place a systemd unit file /opt/feedletter, and then symlink it from /etc/systemd/system.

[Unit]
Description=Feedletter RSS-To-Mail-Etc Service
After=syslog.target network.target

[Service]
Type=forking
PIDFile=/opt/feedletter/feedletter.pid
User=feedletter
Group=feedletter
WorkingDirectory=/opt/feedletter

#
# feedletter daemon behaves as a traditional forking service
# when given the --fork flag
#
# its launcher, mill, will fork a feedletter process,
# write a PID file for that process in the base working directory
# and then exit
#
ExecStart=/opt/feedletter/feedletter daemon --fork

TimeoutStopSec=90
Restart=on-failure
RestartSec=10s
StandardError=journal
StandardOutput=journal
StandardInput=null

[Install]
WantedBy=multi-user.target

Then it's just

# systemctl enable feedletter
# systemctl start feedletter

We verify that the service is running.

# journalctl -u feedletter --follow
Jan 13 20:15:55 temp-feedletter systemd[1]: feedletter.service: Consumed 18.609s CPU time, 222M memory peak.
Jan 13 20:15:55 temp-feedletter systemd[1]: Starting feedletter.service - Feedletter RSS-To-Mail-Etc Service...
Jan 13 20:15:56 temp-feedletter feedletter[70034]: =========================================== runMainDaemon PreMain daemon --fork ======================================
Jan 13 20:15:56 temp-feedletter feedletter[70034]: ======================================================================================================================
Jan 13 20:15:59 temp-feedletter feedletter[70034]: [62/62] =================================== runMainDaemon PreMain daemon --fork =================================== 3s
Jan 13 20:15:59 temp-feedletter feedletter[70034]: ======================================================================================================================
Jan 13 20:16:00 temp-feedletter systemd[1]: Started feedletter.service - Feedletter RSS-To-Mail-Etc Service.
Jan 13 20:16:01 temp-feedletter feedletter[70086]: Jan 13, 2025 8:16:01 PM com.mchange.v2.log.MLog
Jan 13 20:16:01 temp-feedletter feedletter[70086]: INFO: MLog clients using java 1.4+ standard logging.
Jan 13 20:16:03 temp-feedletter feedletter[70086]: 2025-01-13@20:16:03 [INFO] [com.mchange.feedletter.Daemon] feedletter-0.1.0-13-60e19c daemon (re)starting.
Jan 13 20:16:03 temp-feedletter feedletter[70086]: 2025-01-13@20:16:03 [INFO] [com.mchange.feedletter.Daemon] Spawning daemon fibers.
Jan 13 20:16:03 temp-feedletter feedletter[70086]: 2025-01-13@20:16:03 [INFO] [com.mchange.feedletter.Daemon] Starting web API service on interface '127.0.0.1', port 8024.
Jan 13 20:16:04 temp-feedletter feedletter[70086]: timestamp=2025-01-13T20:16:04.337801942Z level=INFO thread=#zio-fiber-614230192 message="Starting the server..." location=com.mchange.feedletter.Daemon.webDaemon file=Daemon.scala line=189
Jan 13 20:16:04 temp-feedletter feedletter[70086]: timestamp=2025-01-13T20:16:04.470643106Z level=INFO thread=#zio-fiber-614230192 message="Server started" location=com.mchange.feedletter.Daemon.webDaemon file=Daemon.scala line=189
Jan 13 20:16:41 temp-feedletter feedletter[70086]: 2025-01-13@20:16:41 [FINE] [com.mchange.feedletter.db.PgDatabase] Deleting any as-yet-unassigned items that have been deleted from feed with ID 1
Jan 13 20:16:41 temp-feedletter feedletter[70086]: 2025-01-13@20:16:41 [INFO] [com.mchange.feedletter.db.PgDatabase] Updated/assigned all items from feed with ID 1, feed URL 'https://www.inoreader.com/stream/user/0000000000/tag/all-articles'

Looks Good!

18. Conclusion

That's it!

It seems like a lot, but it's much easier done than said. Give it a try. It won't bite.

It is also possible to customize the announcement messages that feedletter syndicates, or to include post text, length permitting. If that would be useful to you, Let me know and I'll add a quick tutorial on that.

2025-01-06

Supporting all-item RSS


In addition to single-item RSS for each post, this blog now offers all-item-rss. Check it out.. It's effectively a full-content archive of the site, in the form of easy-to-parse RSS.

drafts.interfluidity.com supports both of these features as well. (Here's the all-items-feed, an example single-item-feed.)

The channel element of these feeds includes metadata describing them. iffy:curation describes how many and what sort of items are included in the feed. iffy:completeness describes how complete each item is, i.e. whether it include full content, or even potentially embeds media.

Here's an example from an all-item feed:

<?xml version='1.0' encoding='UTF-8'?>
<rss version="2.0" xmlns:iffy="http://tech.interfluidity.com/xml/iffy/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    ...
    <iffy:curation>
      <iffy:all/>
    </iffy:curation>
    <iffy:completeness>Content</iffy:completeness>
    ...
  </channel>
</rss>

Here is an example from a single-item feed:

<?xml version='1.0' encoding='UTF-8'?>
<rss version="2.0" xmlns:iffy="http://tech.interfluidity.com/xml/iffy/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    ...
    <iffy:curation>
      <iffy:single/>
    </iffy:curation>
    <iffy:completeness>Content</iffy:completeness>
    ...
  </channel>
</rss>

An ordinary syndicatin feed that includes curation and completeness metadata looks like this:

<?xml version='1.0' encoding='UTF-8'?>
<rss version="2.0" xmlns:iffy="http://tech.interfluidity.com/xml/iffy/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    ...
    <iffy:curation>
      <iffy:recent last="5"/>
    </iffy:curation>
    <iffy:completeness>Content</iffy:completeness>
    ...
  </channel>
</rss>

All-item and single-item feeds, like syndication feeds, are discoverable in <link rel="alternate"> elements of the HTML pages of the blog.

But they are not intended to be discovered by feed readers, which expect short feeds of recent posts for syndication. In order to prevent discovery of these feeds by feed readers, we give them special MIME type in HTML link elements only. These are still RSS feeds. Their "true" MIME type is still application/rss+xml.

But in HTML link elements — only in HTML link elements — I use more specific, less standard MIME types, to throw unwitting feed readers off the scent. These MIME types are application/x-all-item-rss+xml and application/x-single-item-rss+xml.

Here's the HTML link element to a site's all-item feed:

  <link rel="alternate" type="application/x-all-item-rss+xml" title="drafts.interfluidity.com - all items" href="https://drafts.interfluidity.com/all-item-feed/index.rss">

Here's the HTML link element from a post permalink to that post's single-item feed:

  <link rel="alternate" type="application/x-single-item-rss+xml" title="Segmentation fault" href="index.rss">

While ordinary feed readers should ignore these feeds, clients resolving iffy:item-ref tags might find single-item and all-item feeds very useful.

2024-12-22
2024-11-15

Updated: The 'iffy' XML namespace


A significant update of The 'iffy' XML namespace was made on 2024-11-15 @ 01:10 AM EST.

→ Document iffy:curation, and curation type elements iffy:all, iffy:recent, iffy:selection, iffy:single.

The post was originally published 2024-05-13 @ 04:10 AM EDT.

2024-11-12

Supporting single-item RSS


This blog now generates single-item RSS alongside each blog post.

This is in preparation for how I mean to support blog-to-blog comments, most directly.

But in general it is offered as an alternative to approaches like microformats.

Microformats exist and other people use them. I feel a bit bad not to adopt them. But my preference is to have meta-data more cleanly separated from displayed HTML, even if it is "POSH". It seems to me like too many concerns are mixed.

If microformats were very widely adopted — particularly if the JVM ecosystem already had well-supported, widely used microformats parsers — I might swallow my own preference and run with them. But I don't think that's the case. I'm going to have to do my own parsing, and I have good tools for that with XML and RSS.

unstatic SimpleBlog instances now support generating single-item RSS. Just override

val generateSingleItemRss : Boolean

to true, and they will appear.

By default, each entry's single-item RSS is placed by converting each whatever.html to whatever.rss, under the same path. (If the leaf of the path does not end with .html, then .rss is simply appended to the full path.)

If you prefer a different scheme, you can override

def singleItemRssSiteRootedFromPermalinkSiteRooted( permalinkSiteRooted : Rooted ) : Rooted

where Rooted is a unstantic.UrlPath.Rooted.

Single-item RSS and the HTML "permalink" for the item should mutually refer to one another.

In the HTML header tag, we add a <link> tag with rel="alternate" to point to the single-item RSS.

We don't want feed readers to subscribe to single-item RSS, so, a bit clumsily, we make up a special content type that feed readers won't recognize, application/x-single-item-rss+xml.

Here's the link you'll find in the source of this post.

<link rel="alternate" type="application/x-single-item-rss+xml" title="Supporting single-item RSS" href="index.rss">

The single-item RSS links back to the HTML as a matter of course, via the standard <link> element within the sole <item>.

Check out the single-item RSS for this post!


Note: If an <atom:link rel="self" /> tag is included in the <channel> element of a single-item RSS stream, the type should be the standard type="application/rss+xml", and definitely not application/x-single-item-rss+xml. (I recommend <atom:link rel="self" /> always be included in the <channel> of an RSS stream, whenever there exists a stable link to the stream.) Single-item RSS is just standard RSS for all purposes other than a feed-reader deciding whether to treat a feed as subscribable.

2024-11-02
2024-08-16

Updated: Neonix


A significant update of Neonix was made on 2024-08-16 @ 11:20 AM EDT.

→ Add update regarding new location of Bill Mill's "modern unix tool list"

The post was originally published 2024-06-06 @ 03:35 PM EDT.

2024-07-01

Updated: The 'iffy' XML namespace


A significant update of The 'iffy' XML namespace was made on 2024-07-01 @ 02:40 PM EDT.

→ Major revision of iffy:synthetic, generalize iffy:initial, iffy:update, iffy:update-history, define iffy:uid.

The post was originally published 2024-05-13 @ 04:10 AM EDT.