### # 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('SBoTools') except ImportError: # Placeholder that allows to run the plugin on a bot # without the i18n module _ = lambda x: x class SBoTools(callbacks.Plugin): """Provides SlackBuilds.org build searches""" 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 getMaxDeps(self, msg): maxdeps = self.registryValue('maxdeps') if msg.channel is None: # private message, increase limit maxdeps *= 10 return maxdeps def InitDB(self): filename = self.registryValue('dbpath') # if updated database exists under as newfilename, it's # complete and flushed, ready to use. the db-creation # script that runs from cron uses a different name for # db-creation, and renames to .new only when finished. # no error-checking (if the rename fails, the bot logs an # exception and someone has to fix it manually). newfilename = filename + ".new" if(os.path.exists(newfilename)): if(self.db is not None): self.db.close() self.db = None os.rename(newfilename, filename) 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 sbosearch(self, irc, msg, args, tflag, terms): """ [] | [-t] [ ...]> Search the SlackBuilds.org build database. Case-insensitive substring match. Without -t, does a search by name only. With -t, searches names, descriptions, and tags. With multiple search terms, results are ANDed together. """ category = None if(not tflag): if(len(terms) > 1): irc.error("only one search term allowed unless using -t flag", Raise=True) t = terms[0].split('/',1) if(len(t) != 1): category = self.getCategory(t[0]) if(category is None): irc.error("invalid category '" + t[0] + "'", Raise=True) terms[0] = t[1] db = self.InitDB(); cursor = db.cursor() tags = False; sql = "select distinct(b.id) from builds b, tags t " sql += "where b.id = t.build_id" if(category is not None): sql += " and b.category=" + str(category) + " " for (term) in terms: tlike = "'%" + term.replace("'", "''") + "%'" if(tflag): sql += " and (b.name like " + tlike + " or b.descrip like " + tlike + " or t.tag like " + tlike + ")" else: sql += " and b.name like " + tlike maxresults = self.getMaxResults(msg) cursor.execute(sql) result = cursor.fetchall() count = len(result) toomany = (count > maxresults) if(toomany): del result[maxresults:] if(len(result) == 0): irc.reply("no results found") else: lines = [] lines.append(ircutils.bold(str(count)) + " results:") for (i) in result: c2 = db.cursor() c2.execute("select b.name, c.name from builds b, categories c where b.category=c.id and b.id=?", (i[0],)) xres = c2.fetchall(); if(len(result) == 1): # if there's only one result, jump straight to its !sboinfo (name, cat) = xres[0] self.SBoInfo(irc, msg, cat + "/" + name) return; for (name, cat) in xres: lines.append(ircutils.bold(cat + "/" + name)) if(toomany): lines.append("...") irc.replies(lines, joiner=' ') sbosearch = thread(wrap(sbosearch, [optional(('literal', ['-t'])), many('somethingWithoutSpaces')])) def SBoInfo(self, irc, msg, catbuild): category = None build = None t = catbuild.split('/',1) if(len(t) == 1): build = catbuild else: category = self.getCategory(t[0]) if(category is None): irc.error("invalid category '" + t[0] + "'", Raise=True) build = t[1] db = self.InitDB(); cursor = db.cursor() sql = """select b.id, b.name, c.name, b.descrip, b.version, m.name, e.addr from builds b, categories c, maintainers m, emails e where """ if(category is not None): sql += "c.id=" + str(category) + " and " sql += """b.category = c.id and b.maintainer = m.id and b.email = e.id and b.name=?""" cursor.execute(sql, (build,)) result = cursor.fetchall() if(len(result) == 0): irc.reply("no results found") else: lines = [] for (bid, bname, cname, bdescrip, bversion, maint, email) in result: lines.append(ircutils.bold(cname + "/" + bname) + " v" + bversion + ": " + bdescrip + ", " + maint + " <" + email + ">.") maxdeps = self.getMaxDeps(msg) cursor.execute("select b.name from builds b, deps d where d.build_id=? and b.id=d.depends_on", (bid,)) depres = cursor.fetchall() if(len(depres) == 0): lines.append("No external deps.") else: depcount = len(depres) toomany = (depcount > maxdeps) if(toomany): del depres[maxdeps:] deps = "" for (depname) in depres: deps += " " + depname[0] if(toomany): deps += " ..." if(depcount == 1): plural = "" else: plural = "s" lines.append(str(depcount) + " dep" + plural + ":" + deps) irc.replies(lines, joiner=' ') def sboinfo(self, irc, msg, args, catbuild): """ [] Show detailed information about a SBo build. The argument must be an exact match (this is not a search). """ self.SBoInfo(irc, msg, catbuild) sboinfo = thread(wrap(sboinfo, ['somethingWithoutSpaces'])) def sbomaint(self, irc, msg, args, terms): """ Show list of SBo builds whose maintainers match the search term. """ term = str.join(' ', terms) like = '%' + term + '%' maxdeps = self.getMaxDeps(msg) db = self.InitDB(); cursor = db.cursor() cursor.execute("select c.name, b.name, m.name, e.addr from categories c, builds b, maintainers m, emails e where c.id=b.category and b.maintainer=m.id and b.email=e.id and (m.name like ? or e.addr like ?) order by m.name, e.addr, b.id", (like, like,)) result = cursor.fetchall() if(len(result) == 0): irc.reply("No matching maintainer names or email addresses") return lines = [] toomany = (len(result) > maxdeps) if(toomany): lines.append(str(len(result)) + " results, showing " + str(maxdeps) + ".") del result[maxdeps:] oldnamemail = "" for (cat, build, maint, email) in result: namemail = maint + " <" + email + ">" if(namemail != oldnamemail): if(oldnamemail != ""): lines.append("|") lines.append(namemail + ":") oldnamemail = namemail lines.append(ircutils.bold(cat + "/" + build)) if(toomany): lines.append("...") irc.replies(lines, joiner=" ") sbomaint = thread(wrap(sbomaint, [many('somethingWithoutSpaces')])) def getDependees(self, rflag, buildid, db): retlist = [] cursor = db.cursor() cursor.execute("select c.name, b.name, build_id from builds b, categories c, deps d where b.category=c.id and b.id=d.build_id and d.depends_on=?", (buildid,)) result = cursor.fetchall() for (cat, name, bid) in result: retlist.append(ircutils.bold(cat + "/" + name)) if(rflag): sublist = self.getDependees(rflag, bid, db) if(len(sublist) > 0): retlist.append("[") retlist.extend(sublist) retlist.append("]") return retlist def sborevdep(self, irc, msg, args, rflag, catbuild): """ [-r] Show builds that depend on . With -r, show builds that depend on each result, recursively. """ category = None build = None t = catbuild.split('/',1) if(len(t) == 1): build = catbuild else: category = self.getCategory(t[0]) if(category is None): irc.error("invalid category '" + t[0] + "'", Raise=True) build = t[1] db = self.InitDB(); cursor = db.cursor() sql = "select b.id from builds b" if(category is None): sql += " where " else: sql += ", categories c where c.id=" + str(category) + " and b.category=c.id and " sql += "b.name=?" cursor.execute(sql, (build,)) result = cursor.fetchall() if(len(result) == 0): irc.reply("no such build: " + catbuild) return lines = self.getDependees(rflag, result[0][0], db) if(len(lines) == 0): irc.reply("nothing depends on " + catbuild) else: irc.replies(lines, joiner=' ') sborevdep = thread(wrap(sborevdep, [optional(('literal', ['-r'])), 'somethingWithoutSpaces'])) Class = SBoTools