aboutsummaryrefslogtreecommitdiff
path: root/SlackTools/plugin.py
blob: 7630976d381778a175b79af4c3d5b4b9cdbc5e95 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
###
# 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):
        """ [<category>/]<packagename>

        Search the Slackware package database for packages named like <packagename>.
        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):
        """ <filename>

        Search the Slackware package database for packages that contain files
        named <filename>. This is a case-sensitive match. If <filename>
        begins with a slash (/), it's an exact match (e.g. /bin/ls will not
        match /bin/lsattr). If the first character is not a slash, it's a
        substring match (so bin/ls will match both /bin/ls and /bin/lsattr).
        """

        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] <file-or-package>

        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):
        """ <filename>

        Search the package database for files named like <filename>.
        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):
        """ <filename>

        Search the package database for files named like */bin/<filename>.
        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):
        """ <package>

        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