### # Copyright (c) 2021, B. Watson # All rights reserved. # # ### import os import glob import subprocess import sys import re import sqlite3 from supybot import utils, plugins, ircutils, callbacks from supybot.commands import * import supybot.utils.minisix as minisix try: from supybot.i18n import PluginInternationalization _ = PluginInternationalization('SlackTools') except ImportError: # Placeholder that allows to run the plugin on a bot # without the i18n module _ = lambda x: x # This code could stand to be refactored. Lots of almost-identical # code in FileQuery and PkgQuery. Also, I'm pretty much a python # n00b, so the code might read a little weird if you're a python # expert (I learned just enough of the language, on the fly, to # get this stuff written). That's also why the database-creation # script is in perl (I'm much more familiar and competent with perl). # notes about the database: # it gets created by the makepkgdb.pl script, which requires: # - PACKAGES.txt and extra/PACKAGES.txt from the installer. # - copies of /var/log/scripts and /var/log/packages from a full # slackware install (including /extra but not /testing nor # /pasture) with no 3rd-party packages installed. # see the comments in makepkgdb.pl for more info. # The database only contains packages from one slackware version # and arch. Currently that's x86_64 14.2. Only packages from the # main slackware64/ and extra/ trees are in the db. If there's any # demand for it, it wouldn't be hard to support multiple slack # versions and/or arches... or add pasture/ and/or testing/ to the db. # It would have been easy to include package sizes in the db, but # I didn't think it would be that useful. # The files table also includes symlinks. # /lib64/incoming/ paths get their /incoming removed before being # inserted in the db. # file paths are stored *with* the leading slash, so e.g. /bin/ls # rather than the bin/ls. This is unlike usual slackpkg and # /var/log/packages usage, but it seems more intuitive for users. class SlackTools(callbacks.Plugin): """Provides Slackware package db lookups""" threaded = True db = None categories = None def getMaxResults(self, msg): maxresults = self.registryValue('maxresults') if msg.channel is None: # private message, increase limit maxresults *= 5 return maxresults def InitDB(self): if self.db is None: filename = self.registryValue('dbpath') if(os.path.exists(filename)): self.db = sqlite3.connect(filename) return self.db def getCategory(self, cat): if self.categories is None: self.categories = {} db = self.InitDB(); cursor = db.cursor() cursor.execute("select id, name from categories"); result = cursor.fetchall() for (id, name) in result: self.categories[name] = id if cat in self.categories: return self.categories[cat] else: return None def PkgQuery(self, irc, msg, searchtype, term): db = self.InitDB(); if db is None: irc.error("slackpkg database doesn't exist", Raise=True) maxresults = self.getMaxResults(msg) cursor = db.cursor() term = term.lower() if(searchtype == 'pkg'): args = term.split('/',1) # no category, search all categories if len(args) == 1: term = "*" + term + "*" cursor.execute("select c.name, p.name, p.descrip from categories c, packages p where c.id=p.category and lower(p.name) glob ? order by c.name, p.name limit ?", (term, maxresults+1)) else: # category given, only search it category = self.getCategory(args[0]) if(category is None): irc.error("invalid category: '" + args[0] + "', valid categories are: " + str.join(" ", sorted(self.categories.keys())), Raise=True) term = args[1] + "*" cursor.execute("select c.name, p.name, p.descrip from categories c, packages p where c.id=p.category and p.category=? and lower(p.name) glob ? order by c.name, p.name limit ?", (category, term, maxresults+1)) # file search (no way to specify category) else: if(term[:1] != '/'): term = '*' + term + '*' cursor.execute("select distinct c.name, p.name, p.descrip from categories c, packages p, files f where c.id=p.category and p.id=f.package and f.path glob ? order by c.name, p.name limit ?", (term, maxresults+1)) result = cursor.fetchall() lines = [] if len(result) == 0: irc.reply("no matching packages") else: for (category, pkg, descrip) in result: lines.append(ircutils.bold(category + "/" + pkg) + ": " + descrip) if(len(result) > maxresults): lines.append("[too many results, only showing first " + str(maxresults) + "]") irc.replies(lines, joiner=' | ') # search for packages by name def pkgsearch(self, irc, msg, args, pkg): """ [/] Search the Slackware package database for packages named like . This is a case-sensitive substring match (e.g. "core" matches "coreutils" and "xapian-core"). You can use * for globbing, e.g. "c*s" to find all packages whose names begin with "c" and end with "s". """ self.PkgQuery(irc, msg, 'pkg', pkg) pkgsearch = thread(wrap(pkgsearch, ['somethingWithoutSpaces'])) # search for packages by contents def filesearch(self, irc, msg, args, file): """ Search the Slackware package database for packages that contain files named . This is a case-sensitive exact match, by default (e.g. "/bin/ls" does not match "/usr/bin/ls" nor "/bin/lsattr"). You can use * for globbing, e.g. "/bin/ls*" to match all files in /bin whose names begin with "ls". """ self.PkgQuery(irc, msg, 'file', file) filesearch = thread(wrap(filesearch, ['somethingWithoutSpaces'])) # run filesearch or pkgsearch, save some typing. def pkg(self, irc, msg, args, flag, term): """ [-f|-s] Without no argument, same as pkgsearch. With -f, same as filesearch. With -s, same as srcsearch. See the help for pkgsearch, filesearch, srcsearch for details. """ if(term is None): term = flag flag = '-p' if(flag == '-f'): self.PkgQuery(irc, msg, 'file', term) elif(flag == '-p'): self.PkgQuery(irc, msg, 'pkg', term) elif(flag == '-s'): self.PkgSrc(irc, msg, args, term) else: irc.error("invalid option '" + flag + "'") pkg = thread(wrap(pkg, ['somethingWithoutSpaces', optional('somethingWithoutSpaces')])) def FileQuery(self, irc, msg, term): db = self.InitDB(); if db is None: irc.error("slackpkg database doesn't exist", Raise=True) maxresults = self.getMaxResults(msg) cursor = db.cursor() cursor.execute("select path, symlink from files where path glob ? order by path limit ?", (term, maxresults+1)) result = cursor.fetchall() lines = [] if len(result) == 0: irc.reply("nothing found") else: for (file,symlink) in result: if(symlink): file = file + " [l]" lines.append(file) if(len(result) > maxresults): lines.append("[too many results, only showing first " + str(maxresults) + "]") irc.replies(lines, joiner=' | ') # subset of locate/slocate/mlocate. def locate(self, irc, msg, args, file): """ Search the package database for files named like . This is a case-sensitive substring match (basically works like the actual 'locate' command). You can use * for globbing. """ self.FileQuery(irc, msg, '*' + file + '*') locate = thread(wrap(locate, ['somethingWithoutSpaces'])) # faux 'which' command. def which(self, irc, msg, args, file): """ Search the package database for files named like */bin/. This is similar to the OS "which" command, except it doesn't search a $PATH, it doesn't know whether the file has +x permission, it can return multiple results, and you can use * for globbing. """ self.FileQuery(irc, msg, '*/bin/' + file) which = thread(wrap(which, ['somethingWithoutSpaces'])) def FindSrc(self, pkg): slackdir = self.registryValue('slackpath') url = self.registryValue('baseurl') self.getCategory('a') # make sure categories is populated path = "/patches/source/" + pkg + "/" if (os.path.isdir(slackdir + path)): return url + path for (dir) in self.categories.keys(): path = "/source/" + dir + "/" + pkg + "/" if (os.path.isdir(slackdir + path)): return url + path path = "/extra/source/" + pkg + "/" if (os.path.isdir(slackdir + path)): return url + path path = "/source/k/packaging-x86_64/" + pkg + "/" if (os.path.isdir(slackdir + path)): return url + path # this is gross and not 100% accurate. g = glob.glob(slackdir + "/source/x/x11/src/*/" + pkg + "-*") if (len(g) > 0): return url + "/source/x/x11/" return None def PkgSrc(self, irc, msg, args, pkg): url = self.FindSrc(pkg) if (url is None): irc.reply("no source found for " + pkg) else: irc.reply(url) # fun fact: there's no source dir for the kernel-source package! # if there's not an individual source dir for an x11/ package, # we get source/x/x11/ as the result. That's where x11.SlackBuild # lives, so it's correct. def srcsearch(self, irc, msg, args, pkg): """ Find the URL for the source directory of a Slackware package. This is the directory containing the SlackBuild. The package name is searched for first in patches/, and then the main slackware64/. """ self.PkgSrc(irc, msg, args, pkg) srcsearch = thread(wrap(srcsearch, ['somethingWithoutSpaces'])) Class = SlackTools