Skip to content

Repository files navigation

linkedin-bot

Publishes one technology article to LinkedIn each morning, at a randomised time.

Post copy is generated by a locally hosted language model. Images are scraped from the source article. No content leaves the machine except the post itself.

Python LLM Scheduler API


Overview

Five technology RSS feeds are the input. One LinkedIn post is the output, published once daily between 08:00 and 08:55 at a time selected at random each morning, so the schedule is not externally predictable.

Post copy is written by llama3.1:8b running locally through Ollama. The accompanying image is the source article's own Open Graph image. No article text, generated draft, or credential is transmitted to a hosted model.

Requirements

  • Ollama, running, with the model available: ollama pull llama3.1:8b
  • A LinkedIn Developer application with both the Share on LinkedIn and Sign In with LinkedIn using OpenID Connect products enabled
  • Python 3, and macOS for launchd scheduling

Installation

pip3 install -r requirements.txt
cp .env.template .env      # add LinkedIn client ID and secret

Authorise once through a browser:

python3 auth.py

This writes .tokens.json. Both that file and .env are gitignored.

Verify end to end before automating:

python3 post.py

Then install the scheduler:

chmod +x setup_launchd.sh && ./setup_launchd.sh

This registers two agents: the scheduler at five-minute intervals, and a daily token check at 08:05.

cron_setup.sh is a superseded approach using fixed times via crontab. setup_launchd.sh replaces it and removes the agents it supersedes. launchd is preferred because cron does not fire missed runs after sleep, which on a laptop results in skipped mornings.

Scheduling

launchd cannot express "at a random time within a window", so the randomisation lives one level above it.

The scheduler runs every five minutes and does nothing until the clock passes a target minute, selected once per day at random and stored in .schedule.json alongside the date. A stale date triggers a new selection.

A slot that fires successfully is marked posted, making the day's remaining runs no-ops. A non-zero exit leaves the slot unposted but is deliberately not retried: the failure conditions here — expired token, unavailable model — persist, so retrying would only multiply the notifications.

Publishing pipeline

Retrieval and ranking. Five feeds — Ars Technica, The Verge, TechCrunch, Wired, MIT Technology Review. Entries are scored on how many of fifteen preferred topics appear in the title or summary, and anything already in .posted_urls.json is excluded.

Generation. The prompt fixes the structure: technical substance, then broader implication ending in a question, then hashtags and the source. Emoji, first-person voice, and promotional openers are prohibited. Three attempts, 90-second timeout.

Image acquisition. og:image, twitter:image, and og:image:secure_url are examined in order. Failing all three, a text card is generated with Pillow; a missing image never blocks publication.

Upload and publication. The asset is registered, the bytes uploaded to the returned URL, and a UGC post created referencing it. The article URL is then appended to .posted_urls.json.

Server-side request forgery protection

The Open Graph URL comes from arbitrary third-party HTML, so a crafted page could direct the bot at an internal service or a host on the local network. Every candidate is resolved to an IP and rejected if private, loopback, link-local, reserved, multicast, or unspecified. Content types are limited to JPEG, PNG, and GIF.

Maintenance

LinkedIn access tokens last roughly 60 days, and re-authorisation on expiry is unavoidable. check_token.py runs daily at 08:05 and raises a macOS notification at seven days, escalating at one day and once expired. Re-authorise with python3 auth.py.

launchctl list | grep linkedinbot     # confirm both agents are loaded
python3 scheduler.py                  # inspect today's scheduled time
python3 check_token.py                # check remaining token validity
tail -f logs/$(date +%F).log          # today's run in detail

Logs are written per day into logs/, which is gitignored.

Troubleshooting

Symptom Cause
Ollama is not running Start ollama serve and confirm the model is available
Publication stopped without visible errors Token expired — run python3 check_token.py
Scheduler runs but nothing publishes The current day's slot is already marked posted
401 from LinkedIn Token expired, or the application lacks the Share on LinkedIn product
All posts are text-only Image scraping is failing or being rejected; the log states the reason
An article published twice .posted_urls.json was deleted — it is the sole deduplication record

Project structure

linkedin-bot/
├── post.py             Retrieval, ranking, generation, image, upload, publication
├── scheduler.py        Daily time selection; invokes post.py once
├── auth.py             One-time OAuth browser flow → .tokens.json
├── check_token.py      Daily expiry notification
├── setup_launchd.sh    Installs both agents — preferred
├── cron_setup.sh       Superseded fixed-time configuration
└── .env.template       LinkedIn client ID and secret

Excluded from version control: .env, .tokens.json, .posted_urls.json, .schedule.json, logs/.

Configuration

Setting Location
Feed list RSS_FEEDS in post.py
Topic preferences PREFERRED_TOPICS in post.py
Tone, length, and structure The prompt in generate_post()
Model selection The model field in the Ollama request
Posting window, or additional windows WINDOWS in scheduler.py — a list of tuples; a second entry produces a second daily post

Contributors

chakri192 Author
aider AI pair programmer

About

Automated LinkedIn tech news bot — fetches RSS, generates posts via local LLM (llama3.1:8b), posts once daily at a randomised time via launchd.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages