aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--alfred/helpstrings.py3
-rw-r--r--alfred/mlmmj.py122
-rw-r--r--alfred/server.py67
3 files changed, 115 insertions, 77 deletions
diff --git a/alfred/helpstrings.py b/alfred/helpstrings.py
index 90c7dbb..e81676b 100644
--- a/alfred/helpstrings.py
+++ b/alfred/helpstrings.py
@@ -1,6 +1,6 @@
TOPICS = "topics: srv, bot, files, irc, coffee, vpn, list, git. type !help <topic> for details"
-SRV_TOPIC = "srv: uptime disk mem load status net top topmem io conns who logs. type !help srv <cmd> for details"
+SRV_TOPIC = "srv: uptime disk mem load status net top topmem io conns who logs scan. type !help srv <cmd> for details"
SRV = {
"uptime": "srv uptime: system uptime and load averages. usage: !srv uptime",
"disk": "srv disk: disk usage for root. usage: !srv disk",
@@ -14,6 +14,7 @@ SRV = {
"conns": "srv conns: TCP connection summary. usage: !srv conns",
"who": "srv who: currently logged-in users. usage: !srv who",
"logs": "srv logs: last n lines from a service log. usage: !srv logs service or !srv logs service n",
+ "scan": "srv scan: run clamscan on a path. usage: !srv scan or !srv scan path (allowed: mail, 0x0 files, tmp, quarantine)",
}
BOT_TOPIC = "bot: list, all, name start stop restart status. type !help bot <cmd> for details"
diff --git a/alfred/mlmmj.py b/alfred/mlmmj.py
index b691a7c..bb5c162 100644
--- a/alfred/mlmmj.py
+++ b/alfred/mlmmj.py
@@ -1,22 +1,18 @@
"""
Sopel plugin: mlmmj mailing list management
- !list subscribe <email> - subscribe (forced, no confirmation email)
- !list unsubscribe <email> - unsubscribe
- !list members - show all subscribers
- !list send <message> - send a message to the list as owner (bypasses moderation)
- !list mod - show pending moderation queue
- !list approve <id> - approve a held post
- !list discard <id> - discard a held post
+ !list subscribe <email> - subscribe to list@gumx.cc
+ !list unsubscribe <email> - unsubscribe
+ !list members - show all subscribers
+ !list send <message> - send a message to the list as owner
"""
-import email.utils, glob, mailbox, os, subprocess, tempfile
+import email.utils, glob, os, subprocess, tempfile
from sopel import plugin
LIST_DIR = "/var/spool/mlmmj/list"
LIST_ADDR = "list@gumx.cc"
OWNER_ADDR = "ahmed@gumx.cc"
-MOD_DIR = os.path.join(LIST_DIR, "moderation")
def _owner(bot, trigger):
@@ -28,9 +24,9 @@ def cmd_list(bot, trigger):
if not _owner(bot, trigger):
return
- args = (trigger.group(2) or "").split(None, 1)
+ args = (trigger.group(2) or "").split(None, 2)
if not args:
- bot.say("usage: !list <subscribe|unsubscribe|members|send|mod|approve|discard>")
+ bot.say("usage: !list <subscribe|unsubscribe|members|send|mod>")
return
action = args[0]
@@ -41,7 +37,7 @@ def cmd_list(bot, trigger):
return
email_addr = args[1].strip()
r = subprocess.run(
- ["/usr/bin/mlmmj-sub", "-L", LIST_DIR, "-a", email_addr, "-s", "-f"],
+ ["mlmmj-sub", "-L", LIST_DIR, "-a", email_addr, "-s", "-f"],
capture_output=True, text=True
)
bot.say(f"subscribed: {email_addr}" if r.returncode == 0
@@ -53,7 +49,7 @@ def cmd_list(bot, trigger):
return
email_addr = args[1].strip()
r = subprocess.run(
- ["/usr/bin/mlmmj-unsub", "-L", LIST_DIR, "-a", email_addr, "-s", "-f"],
+ ["mlmmj-unsub", "-L", LIST_DIR, "-a", email_addr, "-s", "-f"],
capture_output=True, text=True
)
bot.say(f"unsubscribed: {email_addr}" if r.returncode == 0
@@ -89,7 +85,7 @@ def cmd_list(bot, trigger):
tmpname = f.name
try:
r = subprocess.run(
- ["/usr/bin/mlmmj-send", "-L", LIST_DIR, "-l", "1", "-m", tmpname],
+ ["mlmmj-send", "-L", LIST_DIR, "-m", tmpname],
capture_output=True, text=True
)
bot.say("sent" if r.returncode == 0
@@ -98,55 +94,55 @@ def cmd_list(bot, trigger):
os.unlink(tmpname)
elif action == "mod":
- files = sorted(os.listdir(MOD_DIR)) if os.path.isdir(MOD_DIR) else []
- if not files:
- bot.say("moderation queue empty")
- return
- for fname in files[:5]:
- fpath = os.path.join(MOD_DIR, fname)
- try:
- with open(fpath, "rb") as f:
- msg = mailbox.Message(f)
- subj = (msg.get("Subject", "(no subject)") or "")[:50]
- frm = (msg.get("From", "") or "")[:30]
- except Exception:
- subj, frm = "(unreadable)", ""
- bot.say(f"{fname} {subj} — {frm}")
- if len(files) > 5:
- bot.say(f"... and {len(files) - 5} more")
-
- elif action == "approve":
- args2 = (trigger.group(2) or "").split(None, 2)
- if len(args2) < 2:
- bot.say("usage: !list approve <id>")
- return
- fid = args2[1].strip()
- path = os.path.join(MOD_DIR, fid)
- if not os.path.isfile(path):
- bot.say(f"not found: {fid}")
- return
- r = subprocess.run(
- ["/usr/bin/mlmmj-send", "-L", LIST_DIR, "-l", "1", "-m", path],
- capture_output=True, text=True
- )
- if r.returncode == 0:
- os.unlink(path)
- bot.say(f"approved: {fid}")
- else:
- bot.say(f"error: {(r.stderr or r.stdout).strip()}")
+ sub = args[1].strip() if len(args) > 1 else ""
+ mod_dir = os.path.join(LIST_DIR, "moderation")
+
+ if not sub or sub == "list":
+ if not os.path.isdir(mod_dir):
+ bot.say("mod: no moderation queue")
+ return
+ items = sorted(os.listdir(mod_dir))
+ if not items:
+ bot.say("mod: queue empty")
+ else:
+ bot.say(f"mod: {len(items)} pending")
+ for name in items[:10]:
+ bot.say(f" {name}")
+
+ elif sub == "approve":
+ if len(args) < 3:
+ bot.say("usage: !list mod approve <filename>")
+ return
+ fname = args[2].strip()
+ fpath = os.path.join(mod_dir, fname)
+ if not os.path.exists(fpath):
+ bot.say(f"mod: not found: {fname}")
+ return
+ r = subprocess.run(
+ ["mlmmj-moderate", "-L", LIST_DIR, "-m", fpath, "-a"],
+ capture_output=True, text=True
+ )
+ bot.say("approved" if r.returncode == 0
+ else f"error: {(r.stderr or r.stdout).strip()}")
- elif action == "discard":
- args2 = (trigger.group(2) or "").split(None, 2)
- if len(args2) < 2:
- bot.say("usage: !list discard <id>")
- return
- fid = args2[1].strip()
- path = os.path.join(MOD_DIR, fid)
- if not os.path.isfile(path):
- bot.say(f"not found: {fid}")
- return
- os.unlink(path)
- bot.say(f"discarded: {fid}")
+ elif sub == "reject":
+ if len(args) < 3:
+ bot.say("usage: !list mod reject <filename>")
+ return
+ fname = args[2].strip()
+ fpath = os.path.join(mod_dir, fname)
+ if not os.path.exists(fpath):
+ bot.say(f"mod: not found: {fname}")
+ return
+ r = subprocess.run(
+ ["mlmmj-moderate", "-L", LIST_DIR, "-m", fpath, "-d"],
+ capture_output=True, text=True
+ )
+ bot.say("rejected" if r.returncode == 0
+ else f"error: {(r.stderr or r.stdout).strip()}")
+
+ else:
+ bot.say("usage: !list mod [list|approve <file>|reject <file>]")
else:
- bot.say("usage: !list <subscribe|unsubscribe|members|send|mod|approve|discard>")
+ bot.say("usage: !list <subscribe|unsubscribe|members|send|mod>")
diff --git a/alfred/server.py b/alfred/server.py
index cfb4c69..a369b88 100644
--- a/alfred/server.py
+++ b/alfred/server.py
@@ -15,6 +15,7 @@ Commands (owner only) - use !server or !srv:
!server conns - TCP connection summary
!server who - logged-in users
!server logs <service> [n] - last n lines of service log (default 10)
+ !server scan [path] - run clamscan on path (default: mail)
"""
import os
@@ -28,18 +29,30 @@ if _alfred_dir not in _sys.path:
import helpstrings as h
SERVICES = [
- "nginx", "postfix", "dovecot", "ngircd", "soju",
+ "nginx", "postfix", "dovecot", "ngircd", "soju", "websocat",
"opendkim", "fcgiwrap", "alfred", "dcron", "0x0",
+ "xandikos", "wg-quick.wg0",
]
LOG_PATHS = {
- "nginx": ["/var/log/nginx/error.log", "/var/log/nginx/access.log"],
- "postfix": ["/var/log/mail.log", "/var/log/messages"],
- "dovecot": ["/var/log/dovecot.log", "/var/log/messages"],
- "ngircd": ["/var/log/ngircd.log", "/var/log/messages"],
- "soju": ["/var/log/soju.log", "/var/log/messages"],
- "alfred": ["/var/log/alfred/sopel.log", "/var/log/alfred/daemon.log"],
- "herald": ["/var/log/herald/sopel.log", "/var/log/herald/daemon.log"],
+ "nginx": ["/var/log/nginx/error.log", "/var/log/nginx/access.log"],
+ "postfix": ["/var/log/mail.log", "/var/log/messages"],
+ "dovecot": ["/var/log/dovecot.log", "/var/log/messages"],
+ "ngircd": ["/var/log/ngircd.log", "/var/log/messages"],
+ "soju": ["/var/log/soju.log"],
+ "websocat": ["/var/log/websocat/daemon.log"],
+ "0x0": ["/var/log/0x0/daemon.log", "/var/log/0x0/prune.log"],
+ "xandikos": ["/var/log/xandikos/daemon.log"],
+ "alfred": ["/var/log/alfred/sopel.log", "/var/log/alfred/daemon.log"],
+ "herald": ["/var/log/herald/sopel.log", "/var/log/herald/daemon.log"],
+ "bikabot": ["/var/log/bikabot/sopel.log", "/var/log/bikabot/daemon.log"],
+ "daft": ["/var/log/daft/sopel.log", "/var/log/daft/daemon.log"],
+ "daffy": ["/var/log/daffy/sopel.log", "/var/log/daffy/daemon.log"],
+ "contessa": ["/var/log/contessa/sopel.log", "/var/log/contessa/daemon.log"],
+ "osterman": ["/var/log/osterman/sopel.log", "/var/log/osterman/daemon.log"],
+ "salvador": ["/var/log/salvador/sopel.log", "/var/log/salvador/daemon.log"],
+ "shireen": ["/var/log/shireen/sopel.log", "/var/log/shireen/daemon.log"],
+ "jeeves": ["/var/log/jeeves/sopel.log", "/var/log/jeeves/daemon.log"],
}
@@ -63,11 +76,13 @@ def _uptime(bot, _args):
def _disk(bot, _args):
- bot.say(_run(["df", "-h", "/"]))
+ for line in _run(["df", "-h", "/"]).splitlines():
+ bot.say(line)
def _mem(bot, _args):
- bot.say(_run(["free", "-h"]))
+ for line in _run(["free", "-h"]).splitlines():
+ bot.say(line)
def _load(bot, _args):
@@ -90,7 +105,7 @@ def _status(bot, args):
capture_output=True, text=True, timeout=5
)
out = (r.stdout + r.stderr).strip()
- state = "started" if "started" in out else "stopped" if "stopped" in out else out[:20]
+ state = "UP" if "started" in out else "DOWN" if "stopped" in out else out[:20]
results.append(f"{svc}:{state}")
bot.say(" | ".join(results))
@@ -201,12 +216,37 @@ def _io(bot, _args):
def _conns(bot, _args):
- bot.say(_run(["ss", "-s"]))
+ for line in _run(["ss", "-s"]).splitlines():
+ bot.say(line)
def _who(bot, _args):
out = _run(["who"])
- bot.say(out if out.strip() else "who: no users logged in")
+ if not out.strip() or out == "(no output)":
+ bot.say("who: no users logged in")
+ return
+ for line in out.splitlines():
+ bot.say(line)
+
+
+def _scan(bot, args):
+ path = args[0] if args else "/home/ahmed/mail"
+ allowed = ("/home/ahmed/mail", "/var/0x0/files", "/tmp", "/var/quarantine")
+ if not any(path.startswith(p) for p in allowed):
+ bot.say(f"scan: path not allowed: {path}")
+ return
+ bot.say(f"scan: starting clamscan on {path} ...")
+ r = subprocess.run(
+ ["clamscan", "-r", "--quiet", "--infected", path],
+ capture_output=True, text=True, timeout=300
+ )
+ out = (r.stdout + r.stderr).strip()
+ if not out:
+ bot.say("scan: no infected files found")
+ else:
+ for line in out.splitlines()[:15]:
+ bot.say(line)
+ bot.say(f"scan: done (exit {r.returncode})")
def _logs(bot, args):
@@ -244,6 +284,7 @@ _SUBCOMMANDS = {
"conns": _conns,
"who": _who,
"logs": _logs,
+ "scan": _scan,
}