A small, modular Slack bot that runs in one container. It connects to Slack over Socket Mode, so it needs outbound internet only: no public IP, port forward or reverse proxy.
Talk to it three ways:
/bot specs 9C4BPM4@sr-bot specs 9C4BPM4in a channel it's in (replies in a thread)- a DM to the bot:
specs 9C4BPM4
/bot help lists every loaded module. /bot version shows the running build.
The stack runs two containers: the bot and a small admin web page at
http://<lxc ip>:8080 (any username; password is ADMIN_PASSWORD in .env,
generated for you by the install script if you leave it blank). From it you can:
- see whether the bot is running and which commit it's on
- check GitHub for new commits and Update now (pulls, rebuilds, restarts)
- restart, stop, start or rebuild the bot
- read the bot's logs and the update log
- change settings (modules, tokens, Dell keys, log level) and restart the bot
Updates also happen on their own: the admin checks GitHub every
ADMIN_AUTO_UPDATE_MINUTES (default 30) and applies new commits on main.
Set it to 0 to update only by hand. Every push to this repo therefore reaches
the running bot within half an hour.
The admin can rebuild containers because it has the Docker socket, which is root-level access to the LXC. Keep port 8080 on your LAN or behind a VPN and don't forward it to the internet.
specs <serial> returns the factory configuration for a system: model, ship
date and warranty end where the vendor provides them, then CPUs, memory,
storage, RAID, graphics, display, network, power and management parts,
grouped and counted. Up to 5 serials per message, separated by spaces or
commas.
specs guesses the vendor from the serial's shape (Dell 7 characters, Lenovo
8, HP 10) and falls back to asking the others. To ask one vendor directly:
dell <service tag>, hp <serial> (or hpe), lenovo <serial>.
| Vendor | Source | Needs |
|---|---|---|
| Dell | Support site's "Original Configuration" (order SKUs) plus its warranty widget, fetched through a hidden Chromium in the container; TechDirect API when keys are set | nothing (about 1 GB of free RAM for the browser; API keys add ship date and skip the browser) |
| Lenovo | pcsupport.lenovo.com + PSREF spec sheets | nothing |
| HP / HPE | PartSurfer's JSON backend (the unit's factory configuration); HPE's PartSurfer pages as fallback | nothing |
The Lenovo and HP lookups use the public endpoints behind the vendors' own
support sites rather than official APIs, so a site redesign can break them.
Both sites sit behind bot protection that rejects plain Python clients, so
requests go through curl_cffi, which presents a real Chrome TLS fingerprint.
HP packaging, labels, cables and firmware placeholders are filtered out of
the list.
Dell without API keys: the hidden browser
Without keys, Dell lookups read the same "Original Configuration" list the support site shows under Parts & Repairs: processor, memory, drives, graphics, network, power supply and OS, grouped by Dell's order SKU families, plus the service end date from the warranty widget.
Dell's bot protection (Akamai) refuses that page unless its JavaScript has
run in a real browser, so the bot image ships a headless Chromium (Playwright)
that is used for Dell only. Each Dell lookup opens the support home in it,
lets the page settle, fetches the configuration from inside the page and
closes the browser again. That takes 5 to 15 seconds and about 300 MB of RAM
while it runs, so give the LXC at least 1.5 GB (pct set <ctid> --memory 1536
on the Proxmox host; the installer now defaults to that and an 8 GB disk).
Set DELL_BROWSER=off (on the admin page) to skip it; Dell lookups then
answer with a "blocked" message unless API keys are set. Dell can change its
protection at any time; if lookups start failing, run the probe below.
Dell's product page has a Product Specifications panel with an Export
link that downloads <service tag>.csv. Paste that file's contents into a DM
with the bot (or drop the file in), with the service tag in front if you
want it in the title, and the bot answers with the same grouped summary as
specs. No keyword needed; the bot recognises the file. Set INTAKE_CHANNEL
on the admin page (a channel ID like C0123ABCD, or #intake) and every
pasted export is also posted there with who submitted it; naming a channel in
the message (#intake before the CSV) overrides the default.
Dropping the file (rather than pasting its text) and posting to channels the
bot hasn't been invited to need two extra scopes, files:read and
chat:write.public. They are in slack-manifest.yml; after updating the
manifest in your Slack app settings, click Reinstall when Slack asks.
Without them, paste the text and /invite @sr-bot to the intake channel.
DM the bot a photo of the serial sticker with the word intake (or type
intake <serial>), and it looks the machine up, creates a public channel
named after it (hp-z2-g8-tower), adds you plus anyone you @mentioned plus
the people in INTAKE_INVITE, sets the topic to the model and serial, and
posts the specs card in the new channel.
intake (photo attached)
intake @alice @bob (photo attached; also add alice and bob)
intake 9C4BPM4 @alice (typed serial)
intake name=brian-laptop (pick the channel name yourself)
If the name is taken the serial's last four characters are appended.
INTAKE_PREFIX (admin page) puts a prefix on every name, e.g. dev-.
Needs the channels:manage, channels:write.invites and users:read
scopes from the manifest (reinstall the app after updating it).
Drop a photo of the service tag or serial sticker on the bot (a DM, or
@sr-bot with the picture attached). A vision model reads the serial,
the bot checks it is serial-shaped, runs the normal lookup and posts the
specs card, so a tech can photograph a label at intake and have the parts
list before the box is unpacked.
It uses the same server as chat (LLM_BASE_URL) with a model that can
see, named in LLM_VISION_MODEL on the admin page. On the Ollama machine:
ollama pull qwen2.5vl:7b
qwen3-vl:8b, llama3.2-vision and minicpm-v work too. Blank turns photo
reading off. Photos need the Slack app's files:read scope (in the
manifest). Big phone photos are shrunk to 1600 px before upload; a close,
straight-on shot of the sticker reads best.
@sr-bot <anything that isn't a command> goes to a language model and the
answer comes back in the thread (/bot ask <question> works too). Any
OpenAI-compatible chat server does: Ollama, LM Studio, llama.cpp's server,
vLLM, or a hosted API. Set on the admin page:
| Setting | Example |
|---|---|
REPLY_IN_THREAD |
off answers @mentions in the channel, on in a thread under the message |
LLM_BASE_URL |
http://192.168.1.50:11434/v1 (Ollama) |
LLM_MODEL |
qwen3:8b, llama3.2:3b, llama3.1:8b (whatever ollama list shows) |
LLM_API_KEY |
blank for Ollama |
LLM_SYSTEM_PROMPT |
who the bot is and how to answer |
Leave the URL blank and chat stays off (unknown messages show help).
The model can use the bot's lookups. Ask @sr-bot what's in the HP with serial MXL2062PMN? and the bot looks the serial up first, lets the model
answer from the real parts list, and attaches the specs card underneath.
Models with tool support (qwen3, llama3.1 and up) can also call the lookup
themselves for serials the bot didn't spot.
Reasoning models such as qwen3 think before they answer; the bot hides the
<think> part and posts only the answer. For quicker replies, add /no_think
to the end of LLM_SYSTEM_PROMPT and qwen3 skips the thinking.
The quickest way to a fast model is Ollama on a machine with a GPU, for
example your desktop: install from https://ollama.com, run ollama pull llama3.2:3b, and start it listening on the LAN (OLLAMA_HOST=0.0.0.0 ollama serve, or set that variable in the Ollama app's settings), then use
http://<that machine's IP>:11434/v1. Ollama on the LXC itself works too
(curl -fsSL https://ollama.com/install.sh | sh inside it, then
http://<lxc ip>:11434/v1), but CPU-only answers take a while and a 3B
model wants about 4 GB of RAM on top of the bot.
Each message is answered on its own for now; the model does not see earlier messages in the thread.
With TechDirect API keys the Dell lookup switches to Dell's official API, which adds ship date and warranty end:
- Sign in at https://techdirect.dell.com and open APIs.
- Request the Warranty API (asset entitlements and asset components). Dell approves requests manually, which can take a few days.
- Put the client ID and secret into
.envasDELL_CLIENT_IDandDELL_CLIENT_SECRET(or on the admin page) and restart the bot.
From the LXC (or any machine with the repo and Docker):
docker compose run --rm slackbot python -m bot.cli specs PF2ABCDETo see where a Dell lookup fails (plain request, then the hidden browser):
docker compose run --rm slackbot python -m bot.probe 9C4BPM4- Go to https://api.slack.com/apps, click Create New App > From an app
manifest, pick your workspace and paste
slack-manifest.yml. - Basic Information > App-Level Tokens: generate a token with the
connections:writescope. That'sSLACK_APP_TOKEN(xapp-...). - Install App: install to the workspace. The Bot User OAuth Token is
SLACK_BOT_TOKEN(xoxb-...). - Invite the bot to channels where you want to @mention it:
/invite @sr-bot.
If you change the slash command name in the manifest, set SLASH_COMMAND to match.
Create the Slack app first (above) so you have both tokens. Then in the Proxmox host shell (Datacenter > your node > Shell):
bash -c "$(curl -fsSL https://raw.githubusercontent.com/Realishbird/SlackBot-SR/main/proxmox/create-lxc.sh)"It asks for your Slack tokens (and Dell keys, optional), creates an
unprivileged Debian 12 LXC with nesting enabled (1 core, 1.5 GB RAM, 8 GB disk,
DHCP on vmbr0, starts on boot), installs Docker inside it, clones this repo
to /opt/slackbot-sr and starts the bot. Change defaults by setting them in
front of the command, for example STORAGE=local-zfs BRIDGE=vmbr1 CTID=150.
The variables are listed at the top of proxmox/create-lxc.sh.
To update later, run this inside the LXC (pct enter <id>):
curl -fsSL https://raw.githubusercontent.com/Realishbird/SlackBot-SR/main/proxmox/install.sh | bashinstall.sh also works on its own in an LXC or VM you made yourself.
Pick one. Both run the same docker compose stack.
Option A: Debian LXC with Docker (lightest)
- In Proxmox, create a container from the Debian 12 template (1 core, 1.5 GB
RAM, 8 GB disk; the RAM is for Dell's hidden browser). Under Options > Features, enable
nesting(andkeyctlif the container is unprivileged). - Start it, open the console and install Docker:
apt update && apt install -y curl git curl -fsSL https://get.docker.com | sh
Option B: a small VM
Any Debian or Ubuntu VM, then the same Docker install as above. Use this if you'd rather not run Docker inside an LXC.
Then, in either one:
git clone https://github.com/Realishbird/SlackBot-SR.git
cd slackbot-sr
cp .env.example .env
nano .env # fill in the tokens
docker compose up -d --build
docker compose logs -fYou should see Loaded module specs and A new session has been established.
Both containers restart on their own after reboots (restart: unless-stopped).
If the repo isn't at /opt/slackbot-sr, set REPO_DIR=/where/it/is in
.env so the admin container can find it.
To update by hand: git pull && docker compose up -d --build, or click
Update now on the admin page.
Drop a file in bot/vendors/ with a Vendor subclass. It gets its own
command (/bot <keyword> <serial>) and joins specs automatically:
# bot/vendors/acme.py
import httpx
from bot.vendors.base import NotFound, SystemSpecs, Vendor
class Acme(Vendor):
keyword = "acme"
name = "Acme"
serial_hint = "serial number"
def looks_like(self, serial):
return serial.startswith("AC") # helps `specs` try Acme first
def lookup(self, serial):
data = httpx.get(f"https://support.acme.example/api/{serial}").json()
if not data:
raise NotFound(serial)
specs = SystemSpecs(vendor=self.name, serial=serial, model=data["model"])
for part in data["parts"]:
specs.add(part["description"], part.get("qty", 1)) # auto-grouped
return specsFor anything that isn't a hardware lookup, drop a file in bot/modules/. Any Module subclass there is loaded
automatically at startup:
# bot/modules/ping.py
from bot.module import Module, Reply
class Ping(Module):
keyword = "ping"
summary = "Check the bot is alive"
usage = "ping"
def setup(self):
# read env vars here; raise to disable the module
pass
def handle(self, args: str, user_id: str) -> Reply:
return Reply("pong")handle gets everything after the keyword. Return plain text, and
optionally Slack blocks for richer formatting. Exceptions are caught and
reported in Slack without crashing the bot. Limit what loads with
ENABLED_MODULES=specs,ping.
pip install -r requirements-dev.txt
python -m pytest