Here is a list of the plugins that have a Web interface:
11 |
12 | %(list)s
13 |
14 |
15 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = lint,format,py37
3 | skip_missing_interpreters = True
4 | skipsdist = True
5 |
6 | [testenv]
7 | commands =
8 | poetry install
9 | poetry run coverage run {envbindir}/supybot-test Fedora
10 | poetry run coverage report
11 | passenv = HOME
12 | allowlist_externals = poetry
13 |
14 | [testenv:lint]
15 | commands =
16 | poetry install
17 | poetry run flake8 {posargs}
18 |
19 | [testenv:format]
20 | commands =
21 | poetry install
22 | poetry run black --check --diff {posargs:.}
23 |
24 | [flake8]
25 | show-source = True
26 | max-line-length = 100
27 | exclude = .git,.tox,dist,*egg
28 |
--------------------------------------------------------------------------------
/Vagrantfile:
--------------------------------------------------------------------------------
1 | # -*- mode: ruby -*-
2 | # vi: set ft=ruby :
3 |
4 | Vagrant.configure(2) do |config|
5 | config.vm.box_url = "https://download.fedoraproject.org/pub/fedora/linux/releases/34/Cloud/x86_64/images/Fedora-Cloud-Base-Vagrant-34-1.2.x86_64.vagrant-libvirt.box"
6 | config.vm.box = "f34-cloud-libvirt"
7 | config.vm.hostname = "irc.supybot.test"
8 | config.vm.synced_folder ".", "/vagrant", type: "sshfs"
9 | config.hostmanager.enabled = true
10 | config.hostmanager.manage_host = true
11 | config.vm.provider :libvirt do |libvirt|
12 | libvirt.cpus = 2
13 | libvirt.memory = 2048
14 | end
15 |
16 | config.vm.provision "ansible" do |ansible|
17 | ansible.playbook = "devel/ansible/playbook.yml"
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - develop
5 | pull_request:
6 | branches:
7 | - develop
8 |
9 | name: Run tests
10 |
11 | jobs:
12 |
13 | checks:
14 | name: checks
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v2
18 |
19 | - name: Run checks with Tox
20 | uses: fedora-python/tox-github-action@main
21 | with:
22 | tox_env: ${{ matrix.tox_env }}
23 | dnf_install: poetry krb5-devel
24 |
25 | strategy:
26 | matrix:
27 | tox_env:
28 | - lint
29 | - format
30 |
31 | unit_tests:
32 | name: Unit tests
33 | runs-on: ubuntu-latest
34 | steps:
35 | - uses: actions/checkout@v2
36 |
37 | - name: Run unit tests with Tox
38 | uses: fedora-python/tox-github-action@main
39 | with:
40 | tox_env: ${{ matrix.tox_env }}
41 | dnf_install: limnoria poetry krb5-devel
42 |
43 | strategy:
44 | matrix:
45 | tox_env:
46 | - py37
47 |
--------------------------------------------------------------------------------
/devel/ansible/roles/dev/files/supybot/data/web/default.css.example:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #F0F0F0;
3 | }
4 |
5 | /************************************
6 | * Classes that plugins should use. *
7 | ************************************/
8 |
9 | /* Error pages */
10 | body.error {
11 | text-align: center;
12 | }
13 | body.error p {
14 | background-color: #FFE0E0;
15 | border: 1px #FFA0A0 solid;
16 | }
17 |
18 | /* Pages that only contain a list. */
19 | .purelisting {
20 | text-align: center;
21 | }
22 | .purelisting ul {
23 | margin: 0;
24 | padding: 0;
25 | }
26 | .purelisting ul li {
27 | margin: 0;
28 | padding: 0;
29 | list-style-type: none;
30 | }
31 |
32 | /* Pages that only contain a table. */
33 | .puretable {
34 | text-align: center;
35 | }
36 | .puretable table
37 | {
38 | width: 100%;
39 | border-collapse: collapse;
40 | text-align: center;
41 | }
42 |
43 | .puretable table th
44 | {
45 | /*color: #039;*/
46 | padding: 10px 8px;
47 | border-bottom: 2px solid #6678b1;
48 | }
49 |
50 | .puretable table td
51 | {
52 | padding: 9px 8px 0px 8px;
53 | border-bottom: 1px solid #ccc;
54 | }
55 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "supybot-fedora"
3 | version = "0.5.3"
4 | description = "A Lemnoria (supybot) plugin for various Fedora actions"
5 |
6 | license = "BSD-3-Clause"
7 |
8 | authors = [
9 | "Mike McGrath ",
10 | "Fedora Infrastructure "
11 | ]
12 |
13 | readme = 'README.md' # Markdown files are supported
14 |
15 | repository = "https://github.com/fedora-infra/supybot-fedora"
16 | homepage = "https://github.com/fedora-infra/supybot-fedora"
17 |
18 | classifiers = [
19 | 'Environment :: Plugins',
20 | 'Programming Language :: Python :: 3',
21 | 'Topic :: Communications :: Chat',
22 | ]
23 |
24 | include = [
25 | "tox.ini",
26 | ]
27 |
28 | [tool.poetry.dependencies]
29 | python = "^3.7"
30 | python-fedora = "^1.0.0"
31 | limnoria = "^2020.07.01"
32 | requests = "^2.24.0"
33 | arrow = "^0.15.7"
34 | packagedb-cli = "^2.14.1"
35 | pyyaml = "^5.3.1"
36 | simplejson = "^3.17.2"
37 | pytz = "^2020.1"
38 | sgmllib3k = "^1.0.0"
39 | fasjson-client = "^1.0.0"
40 |
41 | [tool.poetry.dev-dependencies]
42 | mock = "^4.0"
43 | black = "^23.0.0"
44 | flake8 = "^3.7"
45 | coverage = "^5.5"
46 | tox = "^4.5.1"
47 |
48 |
49 | [tool.poetry.plugins."limnoria.plugins"]
50 | "Fedora" = "supybot_fedora"
51 |
52 | [build-system]
53 | requires = ["poetry>=1.0.0"]
54 | build-backend = "poetry.masonry.api"
55 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | supybot-fedora is a Limnoria (supybot) plugin for general Fedora Community
2 | actions. It implements the following commands:
3 |
4 | * admins
5 | * badges
6 | * dctime
7 | * fas
8 | * fasinfo
9 | * group
10 | * hellomynameis
11 | * himynameis
12 | * karma
13 | * localtime
14 | * members
15 | * mirroradmins
16 | * nextmeeting
17 | * nextmeetings
18 | * pulls
19 | * pushduty
20 | * quote
21 | * refresh
22 | * showticket
23 | * sponsors
24 | * swedish
25 | * vacation
26 | * what
27 | * whoowns
28 | * wiki
29 | * wikilink
30 |
31 | # Development Environment
32 |
33 | Vagrant allows contributors to get quickly up and running with a Noggin development
34 | environment by automatically configuring a virtual machine. To get started, first
35 | install the Vagrant and Virtualization packages needed, and start the libvirt
36 | service:
37 |
38 | ```
39 | $ sudo dnf install ansible libvirt vagrant-libvirt vagrant-sshfs vagrant-hostmanager
40 | $ sudo systemctl enable libvirtd
41 | $ sudo systemctl start libvirtd
42 | ```
43 |
44 | Check out the code and run vagrant up:
45 |
46 | ```
47 | $ git clone https://github.com/fedora-infra/supybot-fedora
48 | $ cd supybot-fedora
49 | $ vagrant up
50 | ```
51 |
52 | To test out the bot, use an IRC client to connect to `irc.supybot.test` with the
53 | nick `dudemcpants`, who has owner permissions over the bot. Finally, join the `#test`
54 | channel, which supybot should also be in.
55 |
56 | in the `#test` channel, prefix commands with `.`, e.g. `.nextmeetings`. But the prefix is
57 | not needed when DMing supybot.
58 |
59 | There are also the following commands to interact with the bot, and the bot logs:
60 |
61 | ```
62 | $ fedora-supybot-start
63 | $ fedora-supybot-stop
64 | $ fedora-supybot-restart
65 | $ fedora-supybot-logs
66 | ```
67 |
--------------------------------------------------------------------------------
/devel/ansible/roles/dev/tasks/main.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Install RPM packages
3 | dnf:
4 | name: ['limnoria', 'ngircd', 'weechat', 'git', 'vim', 'poetry', 'krb5-workstation', 'krb5-devel', 'gcc', 'gssproxy', 'ipa-client']
5 | state: present
6 |
7 | - name: set ngircd config
8 | copy:
9 | src: ngircd.conf
10 | dest: /etc/ngircd.conf
11 |
12 | - name: Start ngircd service using systemd
13 | systemd:
14 | state: started
15 | name: ngircd
16 | daemon_reload: yes
17 | enabled: yes
18 |
19 | - name: install python deps with poetry
20 | shell: poetry install
21 | become: yes
22 | become_user: vagrant
23 | args:
24 | chdir: /vagrant/
25 |
26 | - name: Uninstall any old clients
27 | shell: ipa-client-install --uninstall --unattended
28 | ignore_errors: yes
29 |
30 | - name: Enroll system as IPA client
31 | shell: ipa-client-install --hostname irc.supybot.test --domain example.test --realm EXAMPLE.TEST --server ipa.example.test -p admin -w adminPassw0rd! -U -N --force-join
32 |
33 | - name: kinit
34 | shell: echo "adminPassw0rd!" | kinit admin@EXAMPLE.TEST
35 |
36 | - name: Create the service in IPA
37 | command: ipa service-add SUPYBOT/irc.supybot.test
38 |
39 | - name: Get service keytab for SUPYBOT
40 | shell: ipa-getkeytab -p SUPYBOT/irc.supybot.test@EXAMPLE.TEST -k /var/lib/gssproxy/supybot.keytab
41 | args:
42 | creates: /var/lib/gssproxy/supybot.keytab
43 |
44 | - name: Set the correct permissions on keytab
45 | file:
46 | path: /var/lib/gssproxy/supybot.keytab
47 | owner: root
48 | group: root
49 | mode: 0640
50 |
51 | - name: Copy gssproxy conf
52 | copy:
53 | src: gssproxy-supybot.conf
54 | dest: /etc/gssproxy/98-supybot.conf
55 | mode: 0644
56 | owner: root
57 | group: root
58 |
59 | - name: Enable and restart GSSProxy
60 | systemd:
61 | state: restarted
62 | name: gssproxy
63 | enabled: yes
64 | daemon_reload: yes
65 |
66 | - name: copy supybot config
67 | copy:
68 | src: supybot
69 | dest: /home/vagrant
70 | owner: vagrant
71 | group: vagrant
72 |
73 | - name: Install the systemd unit files for the supybot service
74 | copy:
75 | src: supybot.service
76 | dest: /etc/systemd/system/supybot.service
77 | mode: 0644
78 |
79 | - name: Start supybot service using systemd
80 | systemd:
81 | state: started
82 | name: supybot
83 | daemon_reload: yes
84 | enabled: yes
85 |
86 | - name: Install the .bashrc
87 | copy:
88 | src: .bashrc
89 | dest: /home/vagrant/.bashrc
90 | mode: 0644
91 | owner: vagrant
92 | group: vagrant
93 |
--------------------------------------------------------------------------------
/.cico.pipeline:
--------------------------------------------------------------------------------
1 | #!groovy
2 |
3 | /**
4 | * This is supybot-fedora's Jenkins Pipeline Jenkinsfile.
5 | *
6 | * You can read documentation about this file at https://jenkins.io/doc/book/pipeline/jenkinsfile/.
7 | * A useful list of plugins can be found here: https://jenkins.io/doc/pipeline/steps/.
8 | *
9 | * For reference, this is the source of the fedoraInfraTox() macro that runs
10 | * tox on mutiple Fedora releases:
11 | * https://github.com/centosci/cico-shared-library/blob/master/vars/fedoraInfraTox.groovy
12 | */
13 |
14 | // fedoraInfraTox{}
15 |
16 | /**
17 | * Distros we want to test on
18 | */
19 | def ACTIVE_DISTROS = ["f31", "f32", "latest"]
20 |
21 |
22 |
23 |
24 | def stages = [:]
25 | def fedora_containers = [
26 | containerTemplate(name: 'jnlp',
27 | image: "docker-registry.default.svc:5000/openshift/cico-workspace:latest",
28 | ttyEnabled: false,
29 | args: '${computer.jnlpmac} ${computer.name}',
30 | workingDir: "/workdir")
31 | ]
32 |
33 | ACTIVE_DISTROS.each { fedora ->
34 | stages["tox-${fedora}"] = {
35 | stage("tox-${fedora}"){
36 | container("${fedora}"){
37 | sh "mkdir -p /workdir/home/${fedora}"
38 | withEnv(["HOME=/workdir/home/${fedora}"]) {
39 | sh "cp -al ./ ../${fedora}/"
40 | dir( "../${fedora}" ){
41 | sh "rm -rf .tox"
42 | try {
43 | sh "tox --skip-missing-interpreters"
44 | githubNotify context: "CI on ${fedora}", status: 'SUCCESS'
45 | } catch(error) {
46 | githubNotify context: "CI on ${fedora}", status: 'FAILURE'
47 | throw error
48 | }
49 | }
50 | }
51 | }
52 | }
53 | }
54 |
55 | fedora_containers.add(containerTemplate(name: "${fedora}",
56 | image: "quay.io/centosci/python-tox:${fedora}",
57 | ttyEnabled: true,
58 | alwaysPullImage: true,
59 | command: "cat",
60 | workingDir: '/workdir'))
61 | }
62 |
63 | podTemplate(name: 'fedora-tox',
64 | label: 'fedora-tox',
65 | cloud: 'openshift',
66 | containers: fedora_containers
67 | ){
68 | node('fedora-tox'){
69 | ansiColor('xterm'){
70 | stage ('checkout'){
71 | checkout scm
72 | }
73 |
74 | parallel stages
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/supybot_fedora/__init__.py:
--------------------------------------------------------------------------------
1 | ###
2 | # Copyright (c) 2007, Mike McGrath
3 | # All rights reserved.
4 | #
5 | # Redistribution and use in source and binary forms, with or without
6 | # modification, are permitted provided that the following conditions are met:
7 | #
8 | # * Redistributions of source code must retain the above copyright notice,
9 | # this list of conditions, and the following disclaimer.
10 | # * Redistributions in binary form must reproduce the above copyright notice,
11 | # this list of conditions, and the following disclaimer in the
12 | # documentation and/or other materials provided with the distribution.
13 | # * Neither the name of the author of this software nor the name of
14 | # contributors to this software may be used to endorse or promote products
15 | # derived from this software without specific prior written consent.
16 | #
17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 | # POSSIBILITY OF SUCH DAMAGE.
28 | ###
29 |
30 | """
31 | Provides an interface to various Fedora related Who-ha
32 | """
33 |
34 | import supybot
35 | import supybot.world as world
36 | import importlib
37 |
38 | # Use this for the version of this plugin. You may wish to put a CVS keyword
39 | # in here if you're keeping the plugin in CVS or some similar system.
40 | __version__ = "0.5.3"
41 |
42 | # Replace this with an appropriate author or supybot.Author instance.
43 | __author__ = supybot.Author("Mike McGrath", "mmcgrath", "mmcgrath@redhat.com")
44 |
45 | # This is a dictionary mapping supybot.Author instances to lists of
46 | # contributions.
47 | __contributors__ = {
48 | supybot.Author("Ian Weller", "ianweller", "ianweller@gmail.com"): [
49 | "secondary maintainer and code sanitizer"
50 | ],
51 | supybot.Author("Ralph Bean", "threebean", "ralph@fedoraproject.org"): [
52 | "tertiary maintainer and reluctant heir"
53 | ],
54 | }
55 |
56 | # This is a url where the most recent plugin package can be downloaded.
57 | __url__ = "" # 'http://supybot.com/Members/yourname/Fedora/download'
58 |
59 | from . import config
60 | from . import plugin
61 |
62 | importlib.reload(plugin) # In case we're being reloaded.
63 | # Add more reloads here if you add third-party modules and want them to be
64 | # reloaded when this plugin is reloaded. Don't forget to import them as well!
65 |
66 | if world.testing: # pragma: no cover
67 | from . import test # noqa: F401
68 |
69 | Class = plugin.Class
70 | configure = config.configure
71 |
72 |
73 | # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:
74 |
--------------------------------------------------------------------------------
/supybot_fedora/test.py:
--------------------------------------------------------------------------------
1 | ###
2 | # Copyright (c) 2007, Mike McGrath
3 | # All rights reserved.
4 | #
5 | # Redistribution and use in source and binary forms, with or without
6 | # modification, are permitted provided that the following conditions are met:
7 | #
8 | # * Redistributions of source code must retain the above copyright notice,
9 | # this list of conditions, and the following disclaimer.
10 | # * Redistributions in binary form must reproduce the above copyright notice,
11 | # this list of conditions, and the following disclaimer in the
12 | # documentation and/or other materials provided with the distribution.
13 | # * Neither the name of the author of this software nor the name of
14 | # contributors to this software may be used to endorse or promote products
15 | # derived from this software without specific prior written consent.
16 | #
17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 | # POSSIBILITY OF SUCH DAMAGE.
28 | ###
29 |
30 | import os
31 | from unittest import mock
32 | from tempfile import TemporaryDirectory
33 |
34 | from supybot import test, world, conf
35 |
36 | world.myVerbose = test.verbosity.MESSAGES
37 |
38 |
39 | class FASJSONResult:
40 | def __init__(self, result):
41 | self.result = result
42 |
43 |
44 | class FedoraTestCase(test.ChannelPluginTestCase):
45 | plugins = ("Fedora",)
46 |
47 | def setUp(self):
48 | conf.supybot.plugins.Fedora.fasjson.refresh_cache_on_startup.setValue(False)
49 | self.fasjson_client = mock.Mock()
50 | with mock.patch(
51 | "supybot_fedora.plugin.fasjson_client"
52 | ) as fasjson_client_module:
53 | fasjson_client_module.Client.return_value = self.fasjson_client
54 | super().setUp()
55 | self.instance = self.irc.getCallback("Fedora")
56 | self.tmpdir = TemporaryDirectory()
57 | conf.supybot.plugins.Fedora.karma.db_path.setValue(
58 | os.path.join(self.tmpdir.name, "karma.db")
59 | )
60 |
61 | def tearDown(self):
62 | self.tmpdir.cleanup()
63 | super().tearDown()
64 |
65 | def testRandom(self):
66 | self.assertRaises(ValueError)
67 |
68 | @mock.patch("supybot_fedora.plugin.Fedora.get_current_release", return_value="f38")
69 | def testKarma(self, mock_get_current_release):
70 | self.instance.users = ["dummy", "test"]
71 | self.instance.nickmap = {"dummy": "dummy"}
72 | expected = (
73 | "Karma for dummy changed to 1 (for the release cycle f38): "
74 | "https://badges.fedoraproject.org/badge/macaron-cookie-i"
75 | )
76 | self.assertResponse("dummy++", expected)
77 |
78 | def testKarmaActorNotInFAS(self):
79 | self.instance.users = ["dummy"]
80 | self.instance.nickmap = {"dummy": "dummy"}
81 | self.assertResponse("dummy++", "Couldn't find test in FAS")
82 |
83 | def testKarmaTargetNotInFAS(self):
84 | self.instance.users = ["test"]
85 | self.instance.nickmap = {}
86 | self.assertResponse("dummy++", "Couldn't find dummy in FAS")
87 |
88 | def testRefreshIRCNickFormat(self):
89 | nickformats = ["irc:/dummy", "irc://irc.libera.chat/dummy"]
90 | for nick in nickformats:
91 | result = FASJSONResult(
92 | [
93 | {
94 | "username": "dummy",
95 | "emails": ["dummy@example.com"],
96 | "ircnicks": [nick],
97 | "human_name": None,
98 | }
99 | ]
100 | )
101 | self.instance.fasjsonclient.list_users.return_value = result
102 | self.instance._refresh()
103 | self.assertEqual(self.instance.users, ["dummy"])
104 | self.assertEqual(self.instance.nickmap, {"dummy": "dummy"})
105 |
106 |
107 | # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:
108 |
--------------------------------------------------------------------------------
/supybot_fedora/config.py:
--------------------------------------------------------------------------------
1 | ###
2 | # Copyright (c) 2007, Mike McGrath
3 | # All rights reserved.
4 | #
5 | # Redistribution and use in source and binary forms, with or without
6 | # modification, are permitted provided that the following conditions are met:
7 | #
8 | # * Redistributions of source code must retain the above copyright notice,
9 | # this list of conditions, and the following disclaimer.
10 | # * Redistributions in binary form must reproduce the above copyright notice,
11 | # this list of conditions, and the following disclaimer in the
12 | # documentation and/or other materials provided with the distribution.
13 | # * Neither the name of the author of this software nor the name of
14 | # contributors to this software may be used to endorse or promote products
15 | # derived from this software without specific prior written consent.
16 | #
17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 | # POSSIBILITY OF SUCH DAMAGE.
28 | ###
29 |
30 | import supybot.conf as conf
31 | import supybot.registry as registry
32 |
33 |
34 | def configure(advanced):
35 | # This will be called by supybot to configure this module. advanced is
36 | # a bool that specifies whether the user identified himself as an advanced
37 | # user or not. You should effect your configuration by manipulating the
38 | # registry as appropriate.
39 | conf.registerPlugin("Fedora", True)
40 |
41 |
42 | Fedora = conf.registerPlugin("Fedora")
43 | conf.registerGlobalValue(
44 | Fedora,
45 | "naked_ping_admonition",
46 | registry.String(
47 | "https://blogs.gnome.org/markmc/2014/02/20/naked-pings/",
48 | """Response to people who use a naked ping in channel.""",
49 | ),
50 | )
51 | conf.registerGlobalValue(
52 | Fedora,
53 | "naked_ping_channel_blacklist",
54 | registry.CommaSeparatedListOfStrings(
55 | "", "List of channels where not to admonish naked pings"
56 | ),
57 | )
58 |
59 | conf.registerGlobalValue(
60 | Fedora,
61 | "use_fasjson",
62 | registry.Boolean(True, "Use FasJSON for accounts data rather than FAS"),
63 | )
64 |
65 | conf.registerGlobalValue(
66 | Fedora,
67 | "fedocal_url",
68 | registry.String(
69 | "https://calendar.fedoraproject.org/",
70 | """URL for fedocal / Fedora Calendar""",
71 | ),
72 | )
73 |
74 | # This is where your configuration variables (if any) should go. For example:
75 | # conf.registerGlobalValue(Fedora, 'someConfigVariableName',
76 | # registry.Boolean(False, """Help for someConfigVariableName."""))
77 | conf.registerGroup(Fedora, "fas")
78 | conf.registerGlobalValue(
79 | Fedora.fas,
80 | "url",
81 | registry.String(
82 | "https://admin.fedoraproject.org/accounts/",
83 | """URL for the Fedora Account System""",
84 | ),
85 | )
86 | conf.registerGlobalValue(
87 | Fedora.fas,
88 | "username",
89 | registry.String("", """Username for the Fedora Account System""", private=True),
90 | )
91 | conf.registerGlobalValue(
92 | Fedora.fas,
93 | "password",
94 | registry.String("", """Password for the Fedora Account System""", private=True),
95 | )
96 |
97 | conf.registerGroup(Fedora, "fasjson")
98 | conf.registerGlobalValue(
99 | Fedora.fasjson,
100 | "url",
101 | registry.String(
102 | "https://fasjson.fedoraproject.org/",
103 | """URL for the FASJSON API""",
104 | ),
105 | )
106 |
107 | conf.registerGlobalValue(
108 | Fedora.fasjson,
109 | "refresh_cache_on_startup",
110 | registry.Boolean(
111 | True,
112 | "Refresh the FASJSON cache on startup. (typically only turned off for testing purposes)",
113 | ),
114 | )
115 |
116 | conf.registerGroup(Fedora, "github")
117 | conf.registerGlobalValue(
118 | Fedora.github,
119 | "oauth_token",
120 | registry.String("", """OAuth Token for the GitHub""", private=True),
121 | )
122 |
123 |
124 | conf.registerGroup(Fedora, "karma")
125 | conf.registerGlobalValue(
126 | Fedora.karma,
127 | "db_path",
128 | registry.String("/var/tmp/supybot-karma.db", """Path to a karma db on disk"""),
129 | )
130 | conf.registerGlobalValue(
131 | Fedora.karma,
132 | "url",
133 | registry.String(
134 | "https://badges.fedoraproject.org/badge/macaron-cookie-i",
135 | """URL to link people to about karma.""",
136 | ),
137 | )
138 | # Here, 'unaddressed' commands are ones that are not directly addressed to the
139 | # supybot nick. I.e., if this is set to False, then you must say
140 | # 'zodbot: pingou++'
141 | # If it it set to True, then you may say
142 | # 'pingou++'
143 | conf.registerGlobalValue(
144 | Fedora.karma,
145 | "unaddressed",
146 | registry.Boolean(True, "Allow unaddressed karma commands"),
147 | )
148 | conf.registerGlobalValue(
149 | Fedora.karma,
150 | "allow_negative",
151 | registry.Boolean(True, "Allow negative karma to be given"),
152 | )
153 |
154 |
155 | # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:
156 |
--------------------------------------------------------------------------------
/devel/ansible/roles/dev/files/supybot/supybot.conf:
--------------------------------------------------------------------------------
1 |
2 | ######
3 | # Although it is technically possible to do so, we do not recommend that
4 | # you edit this file with a text editor.
5 | # Whenever possible, do it on IRC using the Config plugin, which
6 | # checks values you set are valid before writing them to the
7 | # configuration.
8 | # Moreover, if you edit this file while the bot is running, your
9 | # changes may be lost.
10 | ######
11 |
12 |
13 | ###
14 | # Determines whether the bot will defend itself against command-
15 | # flooding.
16 | #
17 | # Default value: True
18 | ###
19 | supybot.abuse.flood.command: True
20 |
21 | ###
22 | # Determines whether the bot will defend itself against invalid command-
23 | # flooding.
24 | #
25 | # Default value: True
26 | ###
27 | supybot.abuse.flood.command.invalid: True
28 |
29 | ###
30 | # Determines how many invalid commands users are allowed per minute. If
31 | # a user sends more than this many invalid commands in any 60 second
32 | # period, they will be ignored for
33 | # supybot.abuse.flood.command.invalid.punishment seconds. Typically,
34 | # this value is lower than supybot.abuse.flood.command.maximum, since
35 | # it's far less likely (and far more annoying) for users to flood with
36 | # invalid commands than for them to flood with valid commands.
37 | #
38 | # Default value: 5
39 | ###
40 | supybot.abuse.flood.command.invalid.maximum: 5
41 |
42 | ###
43 | # Determines whether the bot will notify people that they're being
44 | # ignored for invalid command flooding.
45 | #
46 | # Default value: True
47 | ###
48 | supybot.abuse.flood.command.invalid.notify: True
49 |
50 | ###
51 | # Determines how many seconds the bot will ignore users who flood it
52 | # with invalid commands. Typically, this value is higher than
53 | # supybot.abuse.flood.command.punishment, since it's far less likely
54 | # (and far more annoying) for users to flood with invalid commands than
55 | # for them to flood with valid commands.
56 | #
57 | # Default value: 600
58 | ###
59 | supybot.abuse.flood.command.invalid.punishment: 600
60 |
61 | ###
62 | # Determines how many commands users are allowed per minute. If a user
63 | # sends more than this many commands in any 60 second period, they will
64 | # be ignored for supybot.abuse.flood.command.punishment seconds.
65 | #
66 | # Default value: 12
67 | ###
68 | supybot.abuse.flood.command.maximum: 12
69 |
70 | ###
71 | # Determines whether the bot will notify people that they're being
72 | # ignored for command flooding.
73 | #
74 | # Default value: True
75 | ###
76 | supybot.abuse.flood.command.notify: True
77 |
78 | ###
79 | # Determines how many seconds the bot will ignore users who flood it
80 | # with commands.
81 | #
82 | # Default value: 300
83 | ###
84 | supybot.abuse.flood.command.punishment: 300
85 |
86 | ###
87 | # Determines the interval used for the history storage.
88 | #
89 | # Default value: 60
90 | ###
91 | supybot.abuse.flood.interval: 60
92 |
93 | ###
94 | # Determines whether the bot will always join a channel when it's
95 | # invited. If this value is False, the bot will only join a channel if
96 | # the user inviting it has the 'admin' capability (or if it's explicitly
97 | # told to join the channel using the Admin.join command).
98 | #
99 | # Default value: False
100 | ###
101 | supybot.alwaysJoinOnInvite: False
102 |
103 | ###
104 | # These are the capabilities that are given to everyone by default. If
105 | # they are normal capabilities, then the user will have to have the
106 | # appropriate anti-capability if you want to override these
107 | # capabilities; if they are anti-capabilities, then the user will have
108 | # to have the actual capability to override these capabilities. See
109 | # docs/CAPABILITIES if you don't understand why these default to what
110 | # they do.
111 | #
112 | # Default value: -scheduler.add -aka.add -scheduler.remove -alias.add -owner -trusted -aka.remove -scheduler.repeat -alias.remove -aka.set -admin
113 | ###
114 | supybot.capabilities: -scheduler.add -aka.add -scheduler.remove -alias.add -owner -trusted -aka.remove -alias.remove -aka.set -admin
115 |
116 | ###
117 | # Determines whether the bot by default will allow users to have a
118 | # capability. If this is disabled, a user must explicitly have the
119 | # capability for whatever command they wish to run. To set this in a
120 | # channel-specific way, use the 'channel capability setdefault' command.
121 | #
122 | # Default value: True
123 | ###
124 | supybot.capabilities.default: True
125 |
126 | ###
127 | # Determines what capabilities the bot will never tell to a non-admin
128 | # whether or not a user has them.
129 | #
130 | # Default value:
131 | ###
132 | #supybot.capabilities.private:
133 |
134 | ###
135 | # These are the capabilities that are given to every authenticated user
136 | # by default. You probably want to use supybot.capabilities instead, to
137 | # give these capabilities both to registered and non-registered users.
138 | #
139 | # Default value:
140 | ###
141 | #supybot.capabilities.registeredUsers:
142 |
143 | ###
144 | # Allows this bot's owner user to use commands that grants them shell
145 | # access. This config variable exists in case you want to prevent MITM
146 | # from the IRC network itself (vulnerable IRCd or IRCops) from gaining
147 | # shell access to the bot's server by impersonating the owner. Setting
148 | # this to False also disables plugins and commands that can be used to
149 | # indirectly gain shell access.
150 | #
151 | # Default value: True
152 | ###
153 | supybot.commands.allowShell: True
154 |
155 | ###
156 | # Determines what commands have default plugins set, and which plugins
157 | # are set to be the default for each of those commands.
158 | ###
159 | supybot.commands.defaultPlugins.addcapability: Admin
160 | supybot.commands.defaultPlugins.capabilities: User
161 | supybot.commands.defaultPlugins.disable: Owner
162 | supybot.commands.defaultPlugins.enable: Owner
163 | supybot.commands.defaultPlugins.help: Misc
164 | supybot.commands.defaultPlugins.ignore: Admin
165 |
166 | ###
167 | # Determines what plugins automatically get precedence over all other
168 | # plugins when selecting a default plugin for a command. By default,
169 | # this includes the standard loaded plugins. You probably shouldn't
170 | # change this if you don't know what you're doing; if you do know what
171 | # you're doing, then also know that this set is case-sensitive.
172 | #
173 | # Default value: Misc Config Channel User Admin Owner
174 | ###
175 | supybot.commands.defaultPlugins.importantPlugins: Misc Config Channel User Admin Owner
176 | supybot.commands.defaultPlugins.list: Misc
177 | supybot.commands.defaultPlugins.reload: Owner
178 | supybot.commands.defaultPlugins.removecapability: Admin
179 | supybot.commands.defaultPlugins.unignore: Admin
180 |
181 | ###
182 | # Determines what commands are currently disabled. Such commands will
183 | # not appear in command lists, etc. They will appear not even to exist.
184 | #
185 | # Default value:
186 | ###
187 | #supybot.commands.disabled:
188 |
189 | ###
190 | # Determines whether the bot will allow nested commands, which rule. You
191 | # definitely should keep this on.
192 | #
193 | # Default value: True
194 | ###
195 | supybot.commands.nested: True
196 |
197 | ###
198 | # Supybot allows you to specify what brackets are used for your nested
199 | # commands. Valid sets of brackets include [], <>, {}, and (). [] has
200 | # strong historical motivation, but <> or () might be slightly superior
201 | # because they cannot occur in a nick. If this string is empty, nested
202 | # commands will not be allowed in this channel.
203 | #
204 | # Default value: []
205 | ###
206 | supybot.commands.nested.brackets: []
207 |
208 | ###
209 | # Determines what the maximum number of nested commands will be; users
210 | # will receive an error if they attempt commands more nested than this.
211 | #
212 | # Default value: 10
213 | ###
214 | supybot.commands.nested.maximum: 10
215 |
216 | ###
217 | # Supybot allows nested commands. Enabling this option will allow nested
218 | # commands with a syntax similar to UNIX pipes, for example: 'bot: foo |
219 | # bar'.
220 | #
221 | # Default value: False
222 | ###
223 | supybot.commands.nested.pipeSyntax: False
224 |
225 | ###
226 | # Determines what characters are valid for quoting arguments to commands
227 | # in order to prevent them from being tokenized.
228 | #
229 | # Default value: "
230 | ###
231 | supybot.commands.quotes: "
232 |
233 | ###
234 | # Determines what databases are available for use. If this value is not
235 | # configured (that is, if its value is empty) then sane defaults will be
236 | # provided.
237 | #
238 | # Default value: anydbm dbm cdb flat pickle
239 | ###
240 | #supybot.databases:
241 |
242 | ###
243 | # Determines what filename will be used for the channels database. This
244 | # file will go into the directory specified by the
245 | # supybot.directories.conf variable.
246 | #
247 | # Default value: channels.conf
248 | ###
249 | supybot.databases.channels.filename: channels.conf
250 |
251 | ###
252 | # Determines what filename will be used for the ignores database. This
253 | # file will go into the directory specified by the
254 | # supybot.directories.conf variable.
255 | #
256 | # Default value: ignores.conf
257 | ###
258 | supybot.databases.ignores.filename: ignores.conf
259 |
260 | ###
261 | # Determines what filename will be used for the networks database. This
262 | # file will go into the directory specified by the
263 | # supybot.directories.conf variable.
264 | #
265 | # Default value: networks.conf
266 | ###
267 | supybot.databases.networks.filename: networks.conf
268 |
269 | ###
270 | # Determines whether database-based plugins that can be channel-specific
271 | # will be so. This can be overridden by individual channels. Do note
272 | # that the bot needs to be restarted immediately after changing this
273 | # variable or your db plugins may not work for your channel; also note
274 | # that you may wish to set
275 | # supybot.databases.plugins.channelSpecific.link appropriately if you
276 | # wish to share a certain channel's databases globally.
277 | #
278 | # Default value: True
279 | ###
280 | supybot.databases.plugins.channelSpecific: True
281 |
282 | ###
283 | # Determines what channel global (non-channel-specific) databases will
284 | # be considered a part of. This is helpful if you've been running
285 | # channel-specific for awhile and want to turn the databases for your
286 | # primary channel into global databases. If
287 | # supybot.databases.plugins.channelSpecific.link.allow prevents linking,
288 | # the current channel will be used. Do note that the bot needs to be
289 | # restarted immediately after changing this variable or your db plugins
290 | # may not work for your channel.
291 | #
292 | # Default value: #
293 | ###
294 | supybot.databases.plugins.channelSpecific.link: #
295 |
296 | ###
297 | # Determines whether another channel's global (non-channel-specific)
298 | # databases will be allowed to link to this channel's databases. Do note
299 | # that the bot needs to be restarted immediately after changing this
300 | # variable or your db plugins may not work for your channel.
301 | #
302 | # Default value: True
303 | ###
304 | supybot.databases.plugins.channelSpecific.link.allow: True
305 |
306 | ###
307 | # Determines whether the bot will require user registration to use 'add'
308 | # commands in database-based Supybot plugins.
309 | #
310 | # Default value: True
311 | ###
312 | supybot.databases.plugins.requireRegistration: True
313 |
314 | ###
315 | # Determines whether CDB databases will be allowed as a database
316 | # implementation.
317 | #
318 | # Default value: True
319 | ###
320 | supybot.databases.types.cdb: True
321 |
322 | ###
323 | # Determines how often CDB databases will have their modifications
324 | # flushed to disk. When the number of modified records is greater than
325 | # this fraction of the total number of records, the database will be
326 | # entirely flushed to disk.
327 | #
328 | # Default value: 0.5
329 | ###
330 | supybot.databases.types.cdb.maximumModifications: 0.5
331 |
332 | ###
333 | # Determines whether the bot will allow users to unregister their users.
334 | # This can wreak havoc with already-existing databases, so by default we
335 | # don't allow it. Enable this at your own risk. (Do also note that this
336 | # does not prevent the owner of the bot from using the unregister
337 | # command.)
338 | #
339 | # Default value: False
340 | ###
341 | supybot.databases.users.allowUnregistration: False
342 |
343 | ###
344 | # Determines what filename will be used for the users database. This
345 | # file will go into the directory specified by the
346 | # supybot.directories.conf variable.
347 | #
348 | # Default value: users.conf
349 | ###
350 | supybot.databases.users.filename: users.conf
351 |
352 | ###
353 | # Determines how long it takes identification to time out. If the value
354 | # is less than or equal to zero, identification never times out.
355 | #
356 | # Default value: 0
357 | ###
358 | supybot.databases.users.timeoutIdentification: 0
359 |
360 | ###
361 | # Determines whether the bot will automatically flush all flushers
362 | # *very* often. Useful for debugging when you don't know what's breaking
363 | # or when, but think that it might be logged.
364 | #
365 | # Default value: False
366 | ###
367 | supybot.debug.flushVeryOften: False
368 |
369 | ###
370 | # Determines whether the bot will automatically thread all commands.
371 | #
372 | # Default value: False
373 | ###
374 | supybot.debug.threadAllCommands: False
375 |
376 | ###
377 | # Determines whether the bot will ignore unidentified users by default.
378 | # Of course, that'll make it particularly hard for those users to
379 | # register or identify with the bot without adding their hostmasks, but
380 | # that's your problem to solve.
381 | #
382 | # Default value: False
383 | ###
384 | supybot.defaultIgnore: False
385 |
386 | ###
387 | # Determines what the default timeout for socket objects will be. This
388 | # means that *all* sockets will timeout when this many seconds has gone
389 | # by (unless otherwise modified by the author of the code that uses the
390 | # sockets).
391 | #
392 | # Default value: 10
393 | ###
394 | supybot.defaultSocketTimeout: 10
395 |
396 | ###
397 | # Determines what directory backup data is put into. Set it to /dev/null
398 | # to disable backup (it is a special value, so it also works on Windows
399 | # and systems without /dev/null).
400 | #
401 | # Default value: backup
402 | ###
403 | supybot.directories.backup: /home/vagrant/supybot/backup
404 |
405 | ###
406 | # Determines what directory configuration data is put into.
407 | #
408 | # Default value: conf
409 | ###
410 | supybot.directories.conf: /home/vagrant/supybot/conf
411 |
412 | ###
413 | # Determines what directory data is put into.
414 | #
415 | # Default value: data
416 | ###
417 | supybot.directories.data: /home/vagrant/supybot/data
418 |
419 | ###
420 | # Determines what directory temporary files are put into.
421 | #
422 | # Default value: tmp
423 | ###
424 | supybot.directories.data.tmp: /home/vagrant/supybot/data/tmp
425 |
426 | ###
427 | # Determines what directory files of the web server (templates, custom
428 | # images, ...) are put into.
429 | #
430 | # Default value: web
431 | ###
432 | supybot.directories.data.web: /home/vagrant/supybot/data/web
433 |
434 | ###
435 | # Determines what directory the bot will store its logfiles in.
436 | #
437 | # Default value: logs
438 | ###
439 | supybot.directories.log: /home/vagrant/supybot/logs
440 |
441 | ###
442 | # Determines what directories the bot will look for plugins in. Accepts
443 | # a comma-separated list of strings. This means that to add another
444 | # directory, you can nest the former value and add a new one. E.g. you
445 | # can say: bot: 'config supybot.directories.plugins [config
446 | # supybot.directories.plugins], newPluginDirectory'.
447 | #
448 | # Default value:
449 | ###
450 | supybot.directories.plugins: /home/vagrant/supybot/plugins
451 |
452 | ###
453 | # Determines the maximum time the bot will wait before attempting to
454 | # reconnect to an IRC server. The bot may, of course, reconnect earlier
455 | # if possible.
456 | #
457 | # Default value: 300.0
458 | ###
459 | supybot.drivers.maxReconnectWait: 300.0
460 |
461 | ###
462 | # Determines what driver module the bot will use. Current, the only (and
463 | # default) driver is Socket.
464 | #
465 | # Default value: default
466 | ###
467 | supybot.drivers.module: default
468 |
469 | ###
470 | # Determines the default length of time a driver should block waiting
471 | # for input.
472 | #
473 | # Default value: 1.0
474 | ###
475 | supybot.drivers.poll: 1.0
476 |
477 | ###
478 | # A string that is the external IP of the bot. If this is the empty
479 | # string, the bot will attempt to find out its IP dynamically (though
480 | # sometimes that doesn't work, hence this variable). This variable is
481 | # not used by Limnoria and its built-in plugins: see
482 | # supybot.protocols.irc.vhost / supybot.protocols.irc.vhost6 to set the
483 | # IRC bind host, and supybot.servers.http.hosts4 /
484 | # supybot.servers.http.hosts6 to set the HTTP server bind host.
485 | #
486 | # Default value:
487 | ###
488 | #supybot.externalIP:
489 |
490 | ###
491 | # Determines whether the bot will periodically flush data and
492 | # configuration files to disk. Generally, the only time you'll want to
493 | # set this to False is when you want to modify those configuration files
494 | # by hand and don't want the bot to flush its current version over your
495 | # modifications. Do note that if you change this to False inside the
496 | # bot, your changes won't be flushed. To make this change permanent, you
497 | # must edit the registry yourself.
498 | #
499 | # Default value: True
500 | ###
501 | supybot.flush: True
502 |
503 | ###
504 | # Determines whether the bot will unidentify someone when that person
505 | # changes their nick. Setting this to True will cause the bot to track
506 | # such changes. It defaults to False for a little greater security.
507 | #
508 | # Default value: False
509 | ###
510 | supybot.followIdentificationThroughNickChanges: False
511 |
512 | ###
513 | # Determines the bot's ident string, if the server doesn't provide one
514 | # by default.
515 | #
516 | # Default value: limnoria
517 | ###
518 | supybot.ident: limnoria
519 |
520 | ###
521 | # Determines the bot's default language if translations exist. Currently
522 | # supported are 'de', 'en', 'es', 'fi', 'fr' and 'it'.
523 | #
524 | # Default value: en
525 | ###
526 | supybot.language: en
527 |
528 | ###
529 | # Determines what the bot's logging format will be. The relevant
530 | # documentation on the available formattings is Python's documentation
531 | # on its logging module.
532 | #
533 | # Default value: %(levelname)s %(asctime)s %(name)s %(message)s
534 | ###
535 | supybot.log.format: %(levelname)s %(asctime)s %(name)s %(message)s
536 |
537 | ###
538 | # Determines what the minimum priority level logged to file will be. Do
539 | # note that this value does not affect the level logged to stdout; for
540 | # that, you should set the value of supybot.log.stdout.level. Valid
541 | # values are DEBUG, INFO, WARNING, ERROR, and CRITICAL, in order of
542 | # increasing priority.
543 | #
544 | # Default value: INFO
545 | ###
546 | supybot.log.level: INFO
547 |
548 | ###
549 | # Determines what the bot's logging format will be. The relevant
550 | # documentation on the available formattings is Python's documentation
551 | # on its logging module.
552 | #
553 | # Default value: %(levelname)s %(asctime)s %(message)s
554 | ###
555 | supybot.log.plugins.format: %(levelname)s %(asctime)s %(message)s
556 |
557 | ###
558 | # Determines whether the bot will separate plugin logs into their own
559 | # individual logfiles.
560 | #
561 | # Default value: False
562 | ###
563 | supybot.log.plugins.individualLogfiles: False
564 |
565 | ###
566 | # Determines whether the bot will log to stdout.
567 | #
568 | # Default value: True
569 | ###
570 | supybot.log.stdout: True
571 |
572 | ###
573 | # Determines whether the bot's logs to stdout (if enabled) will be
574 | # colorized with ANSI color.
575 | #
576 | # Default value: False
577 | ###
578 | supybot.log.stdout.colorized: False
579 |
580 | ###
581 | # Determines what the bot's logging format will be. The relevant
582 | # documentation on the available formattings is Python's documentation
583 | # on its logging module.
584 | #
585 | # Default value: %(levelname)s %(asctime)s %(message)s
586 | ###
587 | supybot.log.stdout.format: %(levelname)s %(asctime)s %(message)s
588 |
589 | ###
590 | # Determines what the minimum priority level logged will be. Valid
591 | # values are DEBUG, INFO, WARNING, ERROR, and CRITICAL, in order of
592 | # increasing priority.
593 | #
594 | # Default value: INFO
595 | ###
596 | supybot.log.stdout.level: INFO
597 |
598 | ###
599 | # Determines whether the bot will wrap its logs when they're output to
600 | # stdout.
601 | #
602 | # Default value: False
603 | ###
604 | supybot.log.stdout.wrap: False
605 |
606 | ###
607 | # Determines the format string for timestamps in logfiles. Refer to the
608 | # Python documentation for the time module to see what formats are
609 | # accepted. If you set this variable to the empty string, times will be
610 | # logged in a simple seconds-since-epoch format.
611 | #
612 | # Default value: %Y-%m-%dT%H:%M:%S
613 | ###
614 | supybot.log.timestampFormat: %Y-%m-%dT%H:%M:%S
615 |
616 | ###
617 | # Determines what networks the bot will connect to.
618 | #
619 | # Default value:
620 | ###
621 | supybot.networks: supybotfedora
622 |
623 | ###
624 | # Determines what certificate file (if any) the bot will use to connect
625 | # with SSL sockets to supybotfedora.
626 | #
627 | # Default value:
628 | ###
629 | #supybot.networks.supybotfedora.certfile:
630 |
631 | ###
632 | # Space-separated list of channels the bot will join only on
633 | # supybotfedora.
634 | #
635 | # Default value:
636 | ###
637 | supybot.networks.supybotfedora.channels: #test
638 |
639 | ###
640 | # Determines what key (if any) will be used to join the channel.
641 | #
642 | # Default value:
643 | ###
644 | #supybot.networks.supybotfedora.channels.key:
645 |
646 | ###
647 | # Determines the bot's ident string, if the server doesn't provide one
648 | # by default. If empty, defaults to supybot.ident.
649 | #
650 | # Default value:
651 | ###
652 | #supybot.networks.supybotfedora.ident:
653 |
654 | ###
655 | # Determines what nick the bot will use on this network. If empty,
656 | # defaults to supybot.nick.
657 | #
658 | # Default value:
659 | ###
660 | #supybot.networks.supybotfedora.nick:
661 |
662 | ###
663 | # Determines what password will be used on supybotfedora. Yes, we know
664 | # that technically passwords are server-specific and not network-
665 | # specific, but this is the best we can do right now.
666 | #
667 | # Default value:
668 | ###
669 | #supybot.networks.supybotfedora.password:
670 |
671 | ###
672 | # Deprecated config value, keep it to False.
673 | #
674 | # Default value: False
675 | ###
676 | supybot.networks.supybotfedora.requireStarttls: False
677 |
678 | ###
679 | # Determines what SASL ECDSA key (if any) will be used on supybotfedora.
680 | # The public key must be registered with NickServ for SASL ECDSA-
681 | # NIST256P-CHALLENGE to work.
682 | #
683 | # Default value:
684 | ###
685 | #supybot.networks.supybotfedora.sasl.ecdsa_key:
686 |
687 | ###
688 | # Determines what SASL mechanisms will be tried and in which order.
689 | #
690 | # Default value: ecdsa-nist256p-challenge external plain
691 | ###
692 | supybot.networks.supybotfedora.sasl.mechanisms: ecdsa-nist256p-challenge external plain
693 |
694 | ###
695 | # Determines what SASL password will be used on supybotfedora.
696 | #
697 | # Default value:
698 | ###
699 | #supybot.networks.supybotfedora.sasl.password:
700 |
701 | ###
702 | # Determines whether the bot will abort the connection if the none of
703 | # the enabled SASL mechanism succeeded.
704 | #
705 | # Default value: False
706 | ###
707 | supybot.networks.supybotfedora.sasl.required: False
708 |
709 | ###
710 | # Determines what SASL username will be used on supybotfedora. This
711 | # should be the bot's account name.
712 | #
713 | # Default value:
714 | ###
715 | #supybot.networks.supybotfedora.sasl.username:
716 |
717 | ###
718 | # Space-separated list of servers the bot will connect to for
719 | # supybotfedora. Each will be tried in order, wrapping back to the first
720 | # when the cycle is completed.
721 | #
722 | # Default value:
723 | ###
724 | supybot.networks.supybotfedora.servers: irc.supybot.test:6667
725 |
726 | ###
727 | # If not empty, determines the hostname of the socks proxy that will be
728 | # used to connect to this network.
729 | #
730 | # Default value:
731 | ###
732 | #supybot.networks.supybotfedora.socksproxy:
733 |
734 | ###
735 | # Determines whether the bot will attempt to connect with SSL sockets to
736 | # supybotfedora.
737 | #
738 | # Default value: True
739 | ###
740 | supybot.networks.supybotfedora.ssl: False
741 |
742 | ###
743 | # A certificate that is trusted to verify certificates of this network
744 | # (aka. Certificate Authority).
745 | #
746 | # Default value:
747 | ###
748 | #supybot.networks.supybotfedora.ssl.authorityCertificate:
749 |
750 | ###
751 | # Space-separated list of fingerprints of trusted certificates for this
752 | # network. Supported hash algorithms are: md5, sha1, sha224, sha256,
753 | # sha384, and sha512. If non-empty, Certification Authority signatures
754 | # will not be used to verify certificates.
755 | #
756 | # Default value:
757 | ###
758 | #supybot.networks.supybotfedora.ssl.serverFingerprints:
759 |
760 | ###
761 | # Determines what user modes the bot will request from the server when
762 | # it first connects. If empty, defaults to supybot.protocols.irc.umodes
763 | #
764 | # Default value:
765 | ###
766 | #supybot.networks.supybotfedora.umodes:
767 |
768 | ###
769 | # Determines the real name which the bot sends to the server. If empty,
770 | # defaults to supybot.user
771 | #
772 | # Default value:
773 | ###
774 | #supybot.networks.supybotfedora.user:
775 |
776 | ###
777 | # Determines the bot's default nick.
778 | #
779 | # Default value: supybot
780 | ###
781 | supybot.nick: supybot
782 |
783 | ###
784 | # Determines what alternative nicks will be used if the primary nick
785 | # (supybot.nick) isn't available. A %s in this nick is replaced by the
786 | # value of supybot.nick when used. If no alternates are given, or if all
787 | # are used, the supybot.nick will be perturbed appropriately until an
788 | # unused nick is found.
789 | #
790 | # Default value: %s` %s_
791 | ###
792 | supybot.nick.alternates: %s` %s_
793 |
794 | ###
795 | # Determines what file the bot should write its PID (Process ID) to, so
796 | # you can kill it more easily. If it's left unset (as is the default)
797 | # then no PID file will be written. A restart is required for changes to
798 | # this variable to take effect.
799 | #
800 | # Default value:
801 | ###
802 | #supybot.pidFile:
803 |
804 | ###
805 | # List of all plugins that were ever loaded. Currently has no effect
806 | # whatsoever. You probably want to use the 'load' or 'unload' commands,
807 | # or edit supybot.plugins. instead of this.
808 | #
809 | # Default value:
810 | ###
811 | supybot.plugins: Misc Utilities Fedora AutoMode NickAuth Network Config Channel User Admin Owner
812 |
813 | ###
814 | # Determines whether this plugin is loaded by default.
815 | ###
816 | supybot.plugins.Admin: True
817 |
818 | ###
819 | # Determines whether this plugin is publicly visible.
820 | #
821 | # Default value: True
822 | ###
823 | supybot.plugins.Admin.public: True
824 |
825 | ###
826 | # Determines whether this plugin is loaded by default.
827 | ###
828 | supybot.plugins.AutoMode: True
829 |
830 | ###
831 | # Determines whether the bot will check for 'alternative capabilities'
832 | # (ie. autoop, autohalfop, autovoice) in addition to/instead of classic
833 | # ones.
834 | #
835 | # Default value: True
836 | ###
837 | supybot.plugins.AutoMode.alternativeCapabilities: True
838 |
839 | ###
840 | # Determines whether the bot will automatically ban people who join the
841 | # channel and are on the banlist.
842 | #
843 | # Default value: True
844 | ###
845 | supybot.plugins.AutoMode.ban: True
846 |
847 | ###
848 | # Determines how many seconds the bot will automatically ban a person
849 | # when banning.
850 | #
851 | # Default value: 86400
852 | ###
853 | supybot.plugins.AutoMode.ban.period: 86400
854 |
855 | ###
856 | # Determines how many seconds the bot will wait before applying a mode.
857 | # Has no effect on bans.
858 | #
859 | # Default value: 0
860 | ###
861 | supybot.plugins.AutoMode.delay: 0
862 |
863 | ###
864 | # Determines whether this plugin is enabled.
865 | #
866 | # Default value: True
867 | ###
868 | supybot.plugins.AutoMode.enable: True
869 |
870 | ###
871 | # Extra modes that will be applied to a user. Example syntax: user1+o-v
872 | # user2+v user3-v
873 | #
874 | # Default value:
875 | ###
876 | #supybot.plugins.AutoMode.extra:
877 |
878 | ###
879 | # Determines whether the bot will "fall through" to halfop/voicing when
880 | # auto-opping is turned off but auto-halfopping/voicing are turned on.
881 | #
882 | # Default value: True
883 | ###
884 | supybot.plugins.AutoMode.fallthrough: True
885 |
886 | ###
887 | # Determines whether the bot will automatically halfop people with the
888 | # ,halfop capability when they join the channel.
889 | #
890 | # Default value: False
891 | ###
892 | supybot.plugins.AutoMode.halfop: False
893 |
894 | ###
895 | # Determines whether the bot will automatically op people with the
896 | # ,op capability when they join the channel.
897 | #
898 | # Default value: False
899 | ###
900 | supybot.plugins.AutoMode.op: False
901 |
902 | ###
903 | # Determines whether this plugin will automode owners even if they don't
904 | # have op/halfop/voice/whatever capability.
905 | #
906 | # Default value: False
907 | ###
908 | supybot.plugins.AutoMode.owner: False
909 |
910 | ###
911 | # Determines whether this plugin is publicly visible.
912 | #
913 | # Default value: True
914 | ###
915 | supybot.plugins.AutoMode.public: True
916 |
917 | ###
918 | # Determines whether the bot will automatically voice people with the
919 | # ,voice capability when they join the channel.
920 | #
921 | # Default value: False
922 | ###
923 | supybot.plugins.AutoMode.voice: False
924 |
925 | ###
926 | # Determines whether this plugin is loaded by default.
927 | ###
928 | supybot.plugins.Channel: True
929 |
930 | ###
931 | # Determines whether the bot will always try to rejoin a channel
932 | # whenever it's kicked from the channel.
933 | #
934 | # Default value: True
935 | ###
936 | supybot.plugins.Channel.alwaysRejoin: True
937 |
938 | ###
939 | # Determines whether the output of 'nicks' will be sent in private. This
940 | # prevents mass-highlights of a channel's users, accidental or on
941 | # purpose.
942 | #
943 | # Default value: True
944 | ###
945 | supybot.plugins.Channel.nicksInPrivate: True
946 |
947 | ###
948 | # Determines what part message should be used by default. If the part
949 | # command is called without a part message, this will be used. If this
950 | # value is empty, then no part message will be used (they are optional
951 | # in the IRC protocol). The standard substitutions ($version, $nick,
952 | # etc.) are all handled appropriately.
953 | #
954 | # Default value: Limnoria $version
955 | ###
956 | supybot.plugins.Channel.partMsg: $version
957 |
958 | ###
959 | # Determines whether this plugin is publicly visible.
960 | #
961 | # Default value: True
962 | ###
963 | supybot.plugins.Channel.public: True
964 |
965 | ###
966 | # Determines how many seconds the bot will wait before rejoining a
967 | # channel if kicked and supybot.plugins.Channel.alwaysRejoin is on.
968 | #
969 | # Default value: 0
970 | ###
971 | supybot.plugins.Channel.rejoinDelay: 0
972 |
973 | ###
974 | # Determines whether this plugin is loaded by default.
975 | ###
976 | supybot.plugins.Config: True
977 |
978 | ###
979 | # Determines whether this plugin is publicly visible.
980 | #
981 | # Default value: True
982 | ###
983 | supybot.plugins.Config.public: True
984 |
985 | ###
986 | # Determines whether this plugin is loaded by default.
987 | ###
988 | supybot.plugins.Fedora: True
989 |
990 | ###
991 | # Password for the Fedora Account System
992 | #
993 | # Default value:
994 | ###
995 | #supybot.plugins.Fedora.fas.password:
996 |
997 | ###
998 | # URL for the Fedora Account System
999 | #
1000 | # Default value: https://admin.fedoraproject.org/accounts/
1001 | ###
1002 | supybot.plugins.Fedora.fas.url: https://admin.fedoraproject.org/accounts/
1003 |
1004 | ###
1005 | # Username for the Fedora Account System
1006 | #
1007 | # Default value:
1008 | ###
1009 | #supybot.plugins.Fedora.fas.username:
1010 |
1011 | ###
1012 | # OAuth Token for the GitHub
1013 | #
1014 | # Default value:
1015 | ###
1016 | #supybot.plugins.Fedora.github.oauth_token:
1017 |
1018 | ###
1019 | # Allow negative karma to be given
1020 | #
1021 | # Default value: True
1022 | ###
1023 | supybot.plugins.Fedora.karma.allow_negative: True
1024 |
1025 | ###
1026 | # Path to a karma db on disk
1027 | #
1028 | # Default value: /var/tmp/supybot-karma.db
1029 | ###
1030 | supybot.plugins.Fedora.karma.db_path: /var/tmp/supybot-karma.db
1031 |
1032 | ###
1033 | # Allow unaddressed karma commands
1034 | #
1035 | # Default value: True
1036 | ###
1037 | supybot.plugins.Fedora.karma.unaddressed: True
1038 |
1039 | ###
1040 | # URL to link people to about karma.
1041 | #
1042 | # Default value: https://badges.fedoraproject.org/badge/macaron-cookie-i
1043 | ###
1044 | supybot.plugins.Fedora.karma.url: https://badges.fedoraproject.org/badge/macaron-cookie-i
1045 |
1046 | ###
1047 | # Response to people who use a naked ping in channel.
1048 | #
1049 | # Default value: https://blogs.gnome.org/markmc/2014/02/20/naked-pings/
1050 | ###
1051 | supybot.plugins.Fedora.naked_ping_admonition: https://blogs.gnome.org/markmc/2014/02/20/naked-pings/
1052 |
1053 | ###
1054 | # List of channels where not to admonish naked pings
1055 | #
1056 | # Default value:
1057 | ###
1058 | #supybot.plugins.Fedora.naked_ping_channel_blacklist:
1059 |
1060 | ###
1061 | # Determines whether this plugin is publicly visible.
1062 | #
1063 | # Default value: True
1064 | ###
1065 | supybot.plugins.Fedora.public: True
1066 |
1067 | ###
1068 | # Determines whether this plugin is loaded by default.
1069 | ###
1070 | supybot.plugins.Misc: True
1071 |
1072 | ###
1073 | # Sets a custom help string, displayed when the 'help' command is called
1074 | # without arguments.
1075 | #
1076 | # Default value:
1077 | ###
1078 | #supybot.plugins.Misc.customHelpString:
1079 |
1080 | ###
1081 | # Determines whether or not the nick will be included in the output of
1082 | # last when it is part of a nested command
1083 | #
1084 | # Default value: False
1085 | ###
1086 | supybot.plugins.Misc.last.nested.includeNick: False
1087 |
1088 | ###
1089 | # Determines whether or not the timestamp will be included in the output
1090 | # of last when it is part of a nested command
1091 | #
1092 | # Default value: False
1093 | ###
1094 | supybot.plugins.Misc.last.nested.includeTimestamp: False
1095 |
1096 | ###
1097 | # Determines whether the bot will list private plugins with the list
1098 | # command if given the --private switch. If this is disabled, non-owner
1099 | # users should be unable to see what private plugins are loaded.
1100 | #
1101 | # Default value: False
1102 | ###
1103 | supybot.plugins.Misc.listPrivatePlugins: False
1104 |
1105 | ###
1106 | # Determines whether the bot will list unloaded plugins with the list
1107 | # command if given the --unloaded switch. If this is disabled, non-owner
1108 | # users should be unable to see what unloaded plugins are available.
1109 | #
1110 | # Default value: False
1111 | ###
1112 | supybot.plugins.Misc.listUnloadedPlugins: False
1113 |
1114 | ###
1115 | # Determines how many messages the bot will issue when using the 'more'
1116 | # command.
1117 | #
1118 | # Default value: 1
1119 | ###
1120 | supybot.plugins.Misc.mores: 1
1121 |
1122 | ###
1123 | # Determines whether this plugin is publicly visible.
1124 | #
1125 | # Default value: True
1126 | ###
1127 | supybot.plugins.Misc.public: True
1128 |
1129 | ###
1130 | # Determines the format string for timestamps in the Misc.last command.
1131 | # Refer to the Python documentation for the time module to see what
1132 | # formats are accepted. If you set this variable to the empty string,
1133 | # the timestamp will not be shown.
1134 | #
1135 | # Default value: [%H:%M:%S]
1136 | ###
1137 | supybot.plugins.Misc.timestampFormat: [%H:%M:%S]
1138 |
1139 | ###
1140 | # Determines whether this plugin is loaded by default.
1141 | ###
1142 | supybot.plugins.Network: True
1143 |
1144 | ###
1145 | # Determines whether this plugin is publicly visible.
1146 | #
1147 | # Default value: True
1148 | ###
1149 | supybot.plugins.Network.public: True
1150 |
1151 | ###
1152 | # Determines whether this plugin is loaded by default.
1153 | ###
1154 | supybot.plugins.NickAuth: True
1155 |
1156 | ###
1157 | # Determines whether this plugin is publicly visible.
1158 | #
1159 | # Default value: True
1160 | ###
1161 | supybot.plugins.NickAuth.public: True
1162 |
1163 | ###
1164 | # Determines whether this plugin is loaded by default.
1165 | ###
1166 | supybot.plugins.Owner: True
1167 |
1168 | ###
1169 | # Determines the format of messages sent by the 'announce' command.
1170 | # $owner may be used for the username of the owner calling this command,
1171 | # and $text for the announcement being made.
1172 | #
1173 | # Default value: Announcement from my owner ($owner): $text
1174 | ###
1175 | supybot.plugins.Owner.announceFormat: Announcement from my owner ($owner): $text
1176 |
1177 | ###
1178 | # Determines whether this plugin is publicly visible.
1179 | #
1180 | # Default value: True
1181 | ###
1182 | supybot.plugins.Owner.public: True
1183 |
1184 | ###
1185 | # Determines what quit message will be used by default. If the quit
1186 | # command is called without a quit message, this will be used. If this
1187 | # value is empty, the nick of the person giving the quit command will be
1188 | # used. The standard substitutions ($version, $nick, etc.) are all
1189 | # handled appropriately.
1190 | #
1191 | # Default value: Limnoria $version
1192 | ###
1193 | supybot.plugins.Owner.quitMsg: $version
1194 |
1195 | ###
1196 | # Determines whether this plugin is loaded by default.
1197 | ###
1198 | supybot.plugins.User: True
1199 |
1200 | ###
1201 | # Determines what message the bot sends when a user isn't identified or
1202 | # recognized.
1203 | #
1204 | # Default value:
1205 | ###
1206 | #supybot.plugins.User.customWhoamiError:
1207 |
1208 | ###
1209 | # Determines whether the output of 'user list' will be sent in private.
1210 | # This prevents mass-highlights of people who use their nick as their
1211 | # bot username.
1212 | #
1213 | # Default value: True
1214 | ###
1215 | supybot.plugins.User.listInPrivate: True
1216 |
1217 | ###
1218 | # Determines whether this plugin is publicly visible.
1219 | #
1220 | # Default value: True
1221 | ###
1222 | supybot.plugins.User.public: True
1223 |
1224 | ###
1225 | # Determines whether this plugin is loaded by default.
1226 | ###
1227 | supybot.plugins.Utilities: True
1228 |
1229 | ###
1230 | # Determines whether this plugin is publicly visible.
1231 | #
1232 | # Default value: True
1233 | ###
1234 | supybot.plugins.Utilities.public: True
1235 |
1236 | ###
1237 | # Determines whether the bot will always load important plugins (Admin,
1238 | # Channel, Config, Misc, Owner, and User) regardless of what their
1239 | # configured state is. Generally, if these plugins are configured not to
1240 | # load, you didn't do it on purpose, and you still want them to load.
1241 | # Users who don't want to load these plugins are smart enough to change
1242 | # the value of this variable appropriately :)
1243 | #
1244 | # Default value: True
1245 | ###
1246 | supybot.plugins.alwaysLoadImportant: True
1247 |
1248 | ###
1249 | # Determines how many bytes the bot will 'peek' at when looking through
1250 | # a URL for a doctype or title or something similar. It'll give up after
1251 | # it reads this many bytes, even if it hasn't found what it was looking
1252 | # for.
1253 | #
1254 | # Default value: 8192
1255 | ###
1256 | supybot.protocols.http.peekSize: 8192
1257 |
1258 | ###
1259 | # Determines what HTTP proxy all HTTP requests should go through. The
1260 | # value should be of the form 'host:port'.
1261 | #
1262 | # Default value:
1263 | ###
1264 | #supybot.protocols.http.proxy:
1265 |
1266 | ###
1267 | # If set, the Accept-Language HTTP header will be set to this value for
1268 | # requests. Useful for overriding the auto-detected language based on
1269 | # the server's location.
1270 | #
1271 | # Default value:
1272 | ###
1273 | #supybot.protocols.http.requestLanguage:
1274 |
1275 | ###
1276 | # Determines what will be used as the default banmask style.
1277 | #
1278 | # Default value: host
1279 | ###
1280 | supybot.protocols.irc.banmask: host
1281 |
1282 | ###
1283 | # Determines what certificate file (if any) the bot will use connect
1284 | # with SSL sockets by default.
1285 | #
1286 | # Default value:
1287 | ###
1288 | #supybot.protocols.irc.certfile:
1289 |
1290 | ###
1291 | # Determines whether the bot will enable draft/experimental extensions
1292 | # of the IRC protocol. Setting this to True may break your bot at any
1293 | # time without warning and/or break your configuration irreversibly. So
1294 | # keep it False unless you know what you are doing.
1295 | #
1296 | # Default value: False
1297 | ###
1298 | supybot.protocols.irc.experimentalExtensions: False
1299 |
1300 | ###
1301 | # Determines how many old messages the bot will keep around in its
1302 | # history. Changing this variable will not take effect on a network
1303 | # until it is reconnected.
1304 | #
1305 | # Default value: 1000
1306 | ###
1307 | supybot.protocols.irc.maxHistoryLength: 1000
1308 |
1309 | ###
1310 | # Determines whether the bot will send PINGs to the server it's
1311 | # connected to in order to keep the connection alive and discover
1312 | # earlier when it breaks. Really, this option only exists for debugging
1313 | # purposes: you always should make it True unless you're testing some
1314 | # strange server issues.
1315 | #
1316 | # Default value: True
1317 | ###
1318 | supybot.protocols.irc.ping: True
1319 |
1320 | ###
1321 | # Determines the number of seconds between sending pings to the server,
1322 | # if pings are being sent to the server.
1323 | #
1324 | # Default value: 120
1325 | ###
1326 | supybot.protocols.irc.ping.interval: 120
1327 |
1328 | ###
1329 | # Determines whether the bot will refuse duplicated messages to be
1330 | # queued for delivery to the server. This is a safety mechanism put in
1331 | # place to prevent plugins from sending the same message multiple times;
1332 | # most of the time it doesn't matter, unless you're doing certain kinds
1333 | # of plugin hacking.
1334 | #
1335 | # Default value: False
1336 | ###
1337 | supybot.protocols.irc.queuing.duplicates: False
1338 |
1339 | ###
1340 | # Determines how many seconds must elapse between JOINs sent to the
1341 | # server.
1342 | #
1343 | # Default value: 0.0
1344 | ###
1345 | supybot.protocols.irc.queuing.rateLimit.join: 0.0
1346 |
1347 | ###
1348 | # Determines whether the bot will strictly follow the RFC; currently
1349 | # this only affects what strings are considered to be nicks. If you're
1350 | # using a server or a network that requires you to message a nick such
1351 | # as services@this.network.server then you you should set this to False.
1352 | #
1353 | # Default value: False
1354 | ###
1355 | supybot.protocols.irc.strictRfc: False
1356 |
1357 | ###
1358 | # A floating point number of seconds to throttle queued messages -- that
1359 | # is, messages will not be sent faster than once per throttleTime
1360 | # seconds.
1361 | #
1362 | # Default value: 1.0
1363 | ###
1364 | supybot.protocols.irc.throttleTime: 1.0
1365 |
1366 | ###
1367 | # Determines what user modes the bot will request from the server when
1368 | # it first connects. Many people might choose +i; some networks allow
1369 | # +x, which indicates to the auth services on those networks that you
1370 | # should be given a fake host.
1371 | #
1372 | # Default value:
1373 | ###
1374 | #supybot.protocols.irc.umodes:
1375 |
1376 | ###
1377 | # Determines what vhost the bot will bind to before connecting a server
1378 | # (IRC, HTTP, ...) via IPv4.
1379 | #
1380 | # Default value:
1381 | ###
1382 | #supybot.protocols.irc.vhost:
1383 |
1384 | ###
1385 | # Determines what vhost the bot will bind to before connecting a server
1386 | # (IRC, HTTP, ...) via IPv6.
1387 | #
1388 | # Default value:
1389 | ###
1390 | #supybot.protocols.irc.vhostv6:
1391 |
1392 | ###
1393 | # Determines whether server certificates will be verified, which checks
1394 | # whether the server certificate is signed by a known certificate
1395 | # authority, and aborts the connection if it is not.
1396 | #
1397 | # Default value: False
1398 | ###
1399 | supybot.protocols.ssl.verifyCertificates: False
1400 |
1401 | ###
1402 | # Format used by generic database plugins (Lart, Dunno, Prase, Success,
1403 | # Quote, ...) to show an entry. You can use the following variables:
1404 | # $type/$types/$Type/$Types (plugin name and variants), $id, $text, $at
1405 | # (creation time), $userid/$username/$nick (author).
1406 | #
1407 | # Default value: $Type #$id: $text (added by $username at $at)
1408 | ###
1409 | supybot.replies.databaseRecord: $Type #$id: $text (added by $username at $at)
1410 |
1411 | ###
1412 | # Determines what error message the bot gives when it wants to be
1413 | # ambiguous.
1414 | ###
1415 | supybot.replies.error: An error has occurred and has been logged. Please\
1416 | contact this bot's administrator for more\
1417 | information.
1418 |
1419 | ###
1420 | # Determines what error message the bot gives to the owner when it wants
1421 | # to be ambiguous.
1422 | ###
1423 | supybot.replies.errorOwner: An error has occurred and has been logged. Check\
1424 | the logs for more information.
1425 |
1426 | ###
1427 | # Determines what generic error message is given when the bot is telling
1428 | # someone that they aren't cool enough to use the command they tried to
1429 | # use, and the author of the code calling errorNoCapability didn't
1430 | # provide an explicit capability for whatever reason.
1431 | ###
1432 | supybot.replies.genericNoCapability: You're missing some capability you\
1433 | need. This could be because you\
1434 | actually possess the anti-capability\
1435 | for the capability that's required of\
1436 | you, or because the channel provides\
1437 | that anti-capability by default, or\
1438 | because the global capabilities include\
1439 | that anti-capability. Or, it could be\
1440 | because the channel or\
1441 | supybot.capabilities.default is set to\
1442 | False, meaning that no commands are\
1443 | allowed unless explicitly in your\
1444 | capabilities. Either way, you can't do\
1445 | what you want to do.
1446 |
1447 | ###
1448 | # Determines what message the bot replies with when someone tries to use
1449 | # a command that requires being identified or having a password and
1450 | # neither credential is correct.
1451 | ###
1452 | supybot.replies.incorrectAuthentication: Your hostmask doesn't match or your\
1453 | password is wrong.
1454 |
1455 | ###
1456 | # Determines what error message is given when the bot is telling someone
1457 | # they aren't cool enough to use the command they tried to use.
1458 | ###
1459 | supybot.replies.noCapability: You don't have the %s capability. If you think\
1460 | that you should have this capability, be sure\
1461 | that you are identified before trying again.\
1462 | The 'whoami' command can tell you if you're\
1463 | identified.
1464 |
1465 | ###
1466 | # Determines what error message the bot replies with when someone tries
1467 | # to accessing some information on a user the bot doesn't know about.
1468 | ###
1469 | supybot.replies.noUser: I can't find %s in my user database. If you didn't\
1470 | give a user name, then I might not know what your\
1471 | user is, and you'll need to identify before this\
1472 | command might work.
1473 |
1474 | ###
1475 | # Determines what error message the bot replies with when someone tries
1476 | # to do something that requires them to be registered but they're not
1477 | # currently recognized.
1478 | ###
1479 | supybot.replies.notRegistered: You must be registered to use this command.\
1480 | If you are already registered, you must\
1481 | either identify (using the identify command)\
1482 | or add a hostmask matching your current\
1483 | hostmask (using the "hostmask add" command).
1484 |
1485 | ###
1486 | # Determines what message the bot sends when it thinks you've
1487 | # encountered a bug that the developers don't know about.
1488 | ###
1489 | supybot.replies.possibleBug: This may be a bug. If you think it is, please\
1490 | file a bug report at\
1491 | .
1492 |
1493 | ###
1494 | # Determines what error messages the bot sends to people who try to do
1495 | # things in a channel that really should be done in private.
1496 | ###
1497 | supybot.replies.requiresPrivacy: That operation cannot be done in a channel.
1498 |
1499 | ###
1500 | # Determines what message the bot replies with when a command succeeded.
1501 | # If this configuration variable is empty, no success message will be
1502 | # sent.
1503 | ###
1504 | supybot.replies.success: The operation succeeded.
1505 |
1506 | ###
1507 | # Determines whether error messages that result from bugs in the bot
1508 | # will show a detailed error message (the uncaught exception) or a
1509 | # generic error message.
1510 | #
1511 | # Default value: False
1512 | ###
1513 | supybot.reply.error.detailed: False
1514 |
1515 | ###
1516 | # Determines whether the bot will send error messages to users in
1517 | # private. You might want to do this in order to keep channel traffic to
1518 | # minimum. This can be used in combination with
1519 | # supybot.reply.error.withNotice.
1520 | #
1521 | # Default value: False
1522 | ###
1523 | supybot.reply.error.inPrivate: False
1524 |
1525 | ###
1526 | # Determines whether the bot will *not* provide details in the error
1527 | # message to users who attempt to call a command for which they do not
1528 | # have the necessary capability. You may wish to make this True if you
1529 | # don't want users to understand the underlying security system
1530 | # preventing them from running certain commands.
1531 | #
1532 | # Default value: False
1533 | ###
1534 | supybot.reply.error.noCapability: False
1535 |
1536 | ###
1537 | # Determines whether the bot will send error messages to users via
1538 | # NOTICE instead of PRIVMSG. You might want to do this so users can
1539 | # ignore NOTICEs from the bot and not have to see error messages; or you
1540 | # might want to use it in combination with supybot.reply.errorInPrivate
1541 | # so private errors don't open a query window in most IRC clients.
1542 | #
1543 | # Default value: False
1544 | ###
1545 | supybot.reply.error.withNotice: False
1546 |
1547 | ###
1548 | # Maximum number of items in a list before the end is replaced with 'and
1549 | # others'. Set to 0 to always show the entire list.
1550 | #
1551 | # Default value: 0
1552 | ###
1553 | supybot.reply.format.list.maximumItems: 0
1554 |
1555 | ###
1556 | # Determines how timestamps printed for human reading should be
1557 | # formatted. Refer to the Python documentation for the time module to
1558 | # see valid formatting characters for time formats.
1559 | #
1560 | # Default value: %Y-%m-%dT%H:%M:%S%z
1561 | ###
1562 | supybot.reply.format.time: %Y-%m-%dT%H:%M:%S%z
1563 |
1564 | ###
1565 | # Determines whether elapsed times will be given as "1 day, 2 hours, 3
1566 | # minutes, and 15 seconds" or as "1d 2h 3m 15s".
1567 | #
1568 | # Default value: False
1569 | ###
1570 | supybot.reply.format.time.elapsed.short: False
1571 |
1572 | ###
1573 | # Determines how urls should be formatted.
1574 | #
1575 | # Default value: <%s>
1576 | ###
1577 | supybot.reply.format.url: <%s>
1578 |
1579 | ###
1580 | # Determines whether the bot will reply privately when replying in a
1581 | # channel, rather than replying to the whole channel.
1582 | #
1583 | # Default value: False
1584 | ###
1585 | supybot.reply.inPrivate: False
1586 |
1587 | ###
1588 | # Determines the absolute maximum length of the bot's reply -- no reply
1589 | # will be passed through the bot with a length greater than this.
1590 | #
1591 | # Default value: 131072
1592 | ###
1593 | supybot.reply.maximumLength: 131072
1594 |
1595 | ###
1596 | # Determines whether the bot will break up long messages into chunks and
1597 | # allow users to use the 'more' command to get the remaining chunks.
1598 | #
1599 | # Default value: True
1600 | ###
1601 | supybot.reply.mores: True
1602 |
1603 | ###
1604 | # Determines how many mores will be sent instantly (i.e., without the
1605 | # use of the more command, immediately when they are formed). Defaults
1606 | # to 1, which means that a more command will be required for all but the
1607 | # first chunk.
1608 | #
1609 | # Default value: 1
1610 | ###
1611 | supybot.reply.mores.instant: 1
1612 |
1613 | ###
1614 | # Determines how long individual chunks will be. If set to 0, uses our
1615 | # super-tweaked, get-the-most-out-of-an-individual-message default.
1616 | #
1617 | # Default value: 0
1618 | ###
1619 | supybot.reply.mores.length: 0
1620 |
1621 | ###
1622 | # Determines what the maximum number of chunks (for use with the 'more'
1623 | # command) will be.
1624 | #
1625 | # Default value: 50
1626 | ###
1627 | supybot.reply.mores.maximum: 50
1628 |
1629 | ###
1630 | # Determines whether the bot will send multi-message replies in a single
1631 | # message. This defaults to True in order to prevent the bot from
1632 | # flooding. If this is set to False the bot will send multi-message
1633 | # replies on multiple lines.
1634 | #
1635 | # Default value: True
1636 | ###
1637 | supybot.reply.oneToOne: True
1638 |
1639 | ###
1640 | # Determines whether the bot will allow you to send channel-related
1641 | # commands outside of that channel. Sometimes people find it confusing
1642 | # if a channel-related command (like Filter.outfilter) changes the
1643 | # behavior of the channel but was sent outside the channel itself.
1644 | #
1645 | # Default value: False
1646 | ###
1647 | supybot.reply.requireChannelCommandsToBeSentInChannel: False
1648 |
1649 | ###
1650 | # Supybot normally replies with the full help whenever a user misuses a
1651 | # command. If this value is set to True, the bot will only reply with
1652 | # the syntax of the command (the first line of the help) rather than the
1653 | # full help.
1654 | #
1655 | # Default value: False
1656 | ###
1657 | supybot.reply.showSimpleSyntax: False
1658 |
1659 | ###
1660 | # Determines what prefix characters the bot will reply to. A prefix
1661 | # character is a single character that the bot will use to determine
1662 | # what messages are addressed to it; when there are no prefix characters
1663 | # set, it just uses its nick. Each character in this string is
1664 | # interpreted individually; you can have multiple prefix chars
1665 | # simultaneously, and if any one of them is used as a prefix the bot
1666 | # will assume it is being addressed.
1667 | #
1668 | # Default value:
1669 | ###
1670 | supybot.reply.whenAddressedBy.chars: .
1671 |
1672 | ###
1673 | # Determines whether the bot will reply when people address it by its
1674 | # nick, rather than with a prefix character.
1675 | #
1676 | # Default value: True
1677 | ###
1678 | supybot.reply.whenAddressedBy.nick: True
1679 |
1680 | ###
1681 | # Determines whether the bot will reply when people address it by its
1682 | # nick at the end of the message, rather than at the beginning.
1683 | #
1684 | # Default value: False
1685 | ###
1686 | supybot.reply.whenAddressedBy.nick.atEnd: False
1687 |
1688 | ###
1689 | # Determines what extra nicks the bot will always respond to when
1690 | # addressed by, even if its current nick is something else.
1691 | #
1692 | # Default value:
1693 | ###
1694 | #supybot.reply.whenAddressedBy.nicks:
1695 |
1696 | ###
1697 | # Determines what strings the bot will reply to when they are at the
1698 | # beginning of the message. Whereas prefix.chars can only be one
1699 | # character (although there can be many of them), this variable is a
1700 | # space-separated list of strings, so you can set something like '@@ ??'
1701 | # and the bot will reply when a message is prefixed by either @@ or ??.
1702 | #
1703 | # Default value:
1704 | ###
1705 | #supybot.reply.whenAddressedBy.strings:
1706 |
1707 | ###
1708 | # Determines whether the bot should attempt to reply to all messages
1709 | # even if they don't address it (either via its nick or a prefix
1710 | # character). If you set this to True, you almost certainly want to set
1711 | # supybot.reply.whenNotCommand to False.
1712 | #
1713 | # Default value: False
1714 | ###
1715 | supybot.reply.whenNotAddressed: False
1716 |
1717 | ###
1718 | # Determines whether the bot will reply with an error message when it is
1719 | # addressed but not given a valid command. If this value is False, the
1720 | # bot will remain silent, as long as no other plugins override the
1721 | # normal behavior.
1722 | #
1723 | # Default value: True
1724 | ###
1725 | supybot.reply.whenNotCommand: True
1726 |
1727 | ###
1728 | # Determines whether the bot will always prefix the user's nick to its
1729 | # reply to that user's command.
1730 | #
1731 | # Default value: True
1732 | ###
1733 | supybot.reply.withNickPrefix: True
1734 |
1735 | ###
1736 | # Determines whether the bot will reply with a notice when replying in a
1737 | # channel, rather than replying with a privmsg as normal.
1738 | #
1739 | # Default value: False
1740 | ###
1741 | supybot.reply.withNotice: False
1742 |
1743 | ###
1744 | # Determines whether the bot will reply with a notice when it is sending
1745 | # a private message, in order not to open a /query window in clients.
1746 | #
1747 | # Default value: True
1748 | ###
1749 | supybot.reply.withNoticeWhenPrivate: True
1750 |
1751 | ###
1752 | # Determines the path of the file served as favicon to browsers.
1753 | #
1754 | # Default value:
1755 | ###
1756 | #supybot.servers.http.favicon:
1757 |
1758 | ###
1759 | # Space-separated list of IPv4 hosts the HTTP server will bind.
1760 | #
1761 | # Default value: 0.0.0.0
1762 | ###
1763 | supybot.servers.http.hosts4: 0.0.0.0
1764 |
1765 | ###
1766 | # Space-separated list of IPv6 hosts the HTTP server will bind.
1767 | #
1768 | # Default value: ::0
1769 | ###
1770 | supybot.servers.http.hosts6: ::0
1771 |
1772 | ###
1773 | # Determines whether the server will stay alive if no plugin is using
1774 | # it. This also means that the server will start even if it is not used.
1775 | #
1776 | # Default value: False
1777 | ###
1778 | supybot.servers.http.keepAlive: False
1779 |
1780 | ###
1781 | # Determines what port the HTTP server will bind.
1782 | #
1783 | # Default value: 8080
1784 | ###
1785 | supybot.servers.http.port: 8080
1786 |
1787 | ###
1788 | # Determines the public URL of the server. By default it is
1789 | # http://:/, but you will want to change this if there
1790 | # is a reverse proxy (nginx, apache, ...) in front of the bot.
1791 | #
1792 | # Default value:
1793 | ###
1794 | #supybot.servers.http.publicUrl:
1795 |
1796 | ###
1797 | # If true, uses IPV6_V6ONLY to disable forwaring of IPv4 traffic to IPv6
1798 | # sockets. On *nix, has the same effect as setting kernel variable
1799 | # net.ipv6.bindv6only to 1.
1800 | #
1801 | # Default value: True
1802 | ###
1803 | supybot.servers.http.singleStack: True
1804 |
1805 | ###
1806 | # A floating point number of seconds to throttle snarfed URLs, in order
1807 | # to prevent loops between two bots snarfing the same URLs and having
1808 | # the snarfed URL in the output of the snarf message.
1809 | #
1810 | # Default value: 10.0
1811 | ###
1812 | supybot.snarfThrottle: 10.0
1813 |
1814 | ###
1815 | # Determines the number of seconds between running the upkeep function
1816 | # that flushes (commits) open databases, collects garbage, and records
1817 | # some useful statistics at the debugging level.
1818 | #
1819 | # Default value: 3600
1820 | ###
1821 | supybot.upkeepInterval: 3600
1822 |
1823 | ###
1824 | # Determines the real name which the bot sends to the server. A standard
1825 | # real name using the current version of the bot will be generated if
1826 | # this is left empty.
1827 | #
1828 | # Default value: Limnoria $version
1829 | ###
1830 | supybot.user: Limnoria $version
1831 |
--------------------------------------------------------------------------------
/supybot_fedora/plugin.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | ###
3 | # Copyright (c) 2007, Mike McGrath
4 | # All rights reserved.
5 | #
6 | # Redistribution and use in source and binary forms, with or without
7 | # modification, are permitted provided that the following conditions are met:
8 | #
9 | # * Redistributions of source code must retain the above copyright notice,
10 | # this list of conditions, and the following disclaimer.
11 | # * Redistributions in binary form must reproduce the above copyright notice,
12 | # this list of conditions, and the following disclaimer in the
13 | # documentation and/or other materials provided with the distribution.
14 | # * Neither the name of the author of this software nor the name of
15 | # contributors to this software may be used to endorse or promote products
16 | # derived from this software without specific prior written consent.
17 | #
18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
22 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28 | # POSSIBILITY OF SUCH DAMAGE.
29 | ###
30 |
31 | import arrow
32 | import copy
33 | import sgmllib
34 | import shelve
35 | import html.entities
36 | import requests
37 | import time
38 |
39 | # Use re2 if present. It is faster.
40 | try:
41 | import re2 as re
42 | except ImportError:
43 | import re
44 |
45 | import supybot.utils as utils
46 | import supybot.conf as conf
47 | import supybot.callbacks as callbacks
48 | import supybot.ircutils as ircutils
49 | import supybot.world as world
50 | from supybot.commands import wrap
51 |
52 | from fedora.client import AppError, AuthError
53 | from fedora.client.fas2 import AccountSystem
54 |
55 | import fasjson_client
56 |
57 | from kitchen.text.converters import to_unicode
58 |
59 | # import fedmsg.config
60 | # import fedmsg.meta
61 |
62 | import simplejson
63 | import urllib.request
64 | import urllib.parse
65 | import urllib.error
66 | import socket
67 | import pytz
68 | import datetime
69 | import yaml
70 |
71 | from itertools import chain
72 | from operator import itemgetter
73 |
74 | SPARKLINE_RESOLUTION = 50
75 |
76 | datagrepper_url = "https://apps.fedoraproject.org/datagrepper/raw"
77 |
78 |
79 | def cmp(a, b):
80 | return (a > b) - (a < b)
81 |
82 |
83 | def datagrepper_query(kwargs):
84 | """Return the count of msgs filtered by kwargs for a given time.
85 |
86 | The arguments for this are a little clumsy; this is imposed on us by
87 | multiprocessing.Pool.
88 | """
89 | start, end = kwargs.pop("start"), kwargs.pop("end")
90 | params = {
91 | "start": time.mktime(start.timetuple()),
92 | "end": time.mktime(end.timetuple()),
93 | }
94 | params.update(kwargs)
95 |
96 | req = requests.get(datagrepper_url, params=params)
97 | json_out = simplejson.loads(req.text)
98 | result = int(json_out["total"])
99 | return result
100 |
101 |
102 | def get_ircnicks(user):
103 | """Extract the IRC nick from the IPA format.
104 |
105 | Supported formats:
106 | - irc:/nickname
107 | - irc://servername.tld/nickname
108 | - nickname
109 | """
110 | if not user.get("ircnicks"):
111 | return []
112 | return [
113 | urllib.parse.urlparse(n).path[1:]
114 | for n in user["ircnicks"]
115 | if n.startswith("irc:/")
116 | ] + [
117 | # Legacy format
118 | n
119 | for n in user["ircnicks"]
120 | if ":/" not in n
121 | ]
122 |
123 |
124 | class WorkerThread(world.SupyThread):
125 | """A simple worker thread for our threadpool."""
126 |
127 | def __init__(self, fn, item, *args, **kwargs):
128 | self.fn = fn
129 | self.item = item
130 | super(WorkerThread, self).__init__(*args, **kwargs)
131 |
132 | def run(self):
133 | self.result = self.fn(self.item)
134 |
135 |
136 | class ThreadPool(object):
137 | """Our very own threadpool implementation.
138 |
139 | We make our own thing because multiprocessing is too heavy.
140 | """
141 |
142 | def map(self, fn, items):
143 | threads = []
144 |
145 | for item in items:
146 | threads.append(WorkerThread(fn=fn, item=item))
147 |
148 | for thread in threads:
149 | thread.start()
150 |
151 | for thread in threads:
152 | thread.join()
153 |
154 | return [thread.result for thread in threads]
155 |
156 |
157 | class Title(sgmllib.SGMLParser):
158 | entitydefs = html.entities.entitydefs.copy()
159 | entitydefs["nbsp"] = " "
160 |
161 | def __init__(self):
162 | self.inTitle = False
163 | self.title = ""
164 | sgmllib.SGMLParser.__init__(self)
165 |
166 | def start_title(self, attrs):
167 | self.inTitle = True
168 |
169 | def end_title(self):
170 | self.inTitle = False
171 |
172 | def unknown_entityref(self, name):
173 | if self.inTitle:
174 | self.title += " "
175 |
176 | def unknown_charref(self, name):
177 | if self.inTitle:
178 | self.title += " "
179 |
180 | def handle_data(self, data):
181 | if self.inTitle:
182 | self.title += data
183 |
184 |
185 | class Fedora(callbacks.Plugin):
186 | """Use this plugin to retrieve Fedora-related information."""
187 |
188 | threaded = True
189 |
190 | def __init__(self, irc):
191 | super(Fedora, self).__init__(irc)
192 |
193 | # caches, automatically downloaded on __init__, manually refreshed on
194 | # .refresh
195 | self.users = None
196 | self.faslist = None
197 | self.nickmap = None
198 |
199 | # To get the information, we need a username and password to FAS.
200 | # DO NOT COMMIT YOUR USERNAME AND PASSWORD TO THE PUBLIC REPOSITORY!
201 | self.fasurl = self.registryValue("fas.url")
202 | self.username = self.registryValue("fas.username")
203 | self.password = self.registryValue("fas.password")
204 |
205 | # Use FASJSON
206 | if self.registryValue("use_fasjson"):
207 | try:
208 | self.fasjsonclient = fasjson_client.Client(
209 | url=self.registryValue("fasjson.url"),
210 | )
211 | except fasjson_client.errors.ClientSetupError as e:
212 | self.log.error(
213 | "Something went wrong setting up "
214 | "fasjson client with error: %s" % e
215 | )
216 | raise
217 | else:
218 | self.fasclient = AccountSystem(
219 | self.fasurl, username=self.username, password=self.password
220 | )
221 |
222 | # URLs
223 | # self.url = {}
224 |
225 | self.github_oauth_token = self.registryValue("github.oauth_token")
226 |
227 | self.karma_tokens = ("++", "--") if self.allow_negative else ("++",)
228 |
229 | self.fedocal_url = self.registryValue("fedocal_url")
230 |
231 | # fetch necessary caches
232 | if self.registryValue("fasjson.refresh_cache_on_startup"):
233 | self._refresh()
234 |
235 | # Pull in /etc/fedmsg.d/ so we can build the fedmsg.meta processors.
236 | # fm_config = fedmsg.config.load_config()
237 | # fedmsg.meta.make_processors(**fm_config)
238 |
239 | def _refresh(self):
240 | self.log.info("Downloading user data")
241 |
242 | if self.registryValue("use_fasjson"):
243 | self.log.info("Caching necessary user data")
244 | self.users = []
245 | self.faslist = {}
246 | self.nickmap = {}
247 | for user in self.fasjsonclient.list_users().result:
248 | name = user["username"]
249 | self.users.append(name)
250 | nicks = get_ircnicks(user)
251 | key = " ".join(
252 | [
253 | user["username"],
254 | user["emails"][0],
255 | user["human_name"] or "",
256 | nicks[0] if nicks else "",
257 | ]
258 | )
259 | value = "%s '%s' <%s>" % (
260 | user["username"],
261 | user["human_name"] or "",
262 | user["emails"][0] or "",
263 | )
264 | self.faslist[key] = value
265 | for nick in nicks:
266 | self.nickmap[nick] = name
267 | else:
268 | # leave this untouched for now, will remove when FAS finally disappears
269 | timeout = socket.getdefaulttimeout()
270 | socket.setdefaulttimeout(None)
271 | try:
272 | request = self.fasclient.send_request(
273 | "/user/list", req_params={"search": "*"}, auth=True, timeout=240
274 | )
275 | users = request["people"] + request["unapproved_people"]
276 | del request
277 | except AuthError:
278 | self.log.info("Error Authorizing to FAS")
279 | users = []
280 |
281 | self.log.info("Caching necessary user data")
282 | self.users = {}
283 | self.faslist = {}
284 | self.nickmap = {}
285 | for user in users:
286 | name = user["username"]
287 | self.users[name] = {}
288 | self.users[name]["id"] = user["id"]
289 | key = " ".join(
290 | [
291 | user["username"],
292 | user["email"] or "",
293 | user["human_name"] or "",
294 | user["ircnick"] or "",
295 | ]
296 | )
297 | key = key.lower()
298 | value = "%s '%s' <%s>" % (
299 | user["username"],
300 | user["human_name"] or "",
301 | user["email"] or "",
302 | )
303 | self.faslist[key] = value
304 | if user["ircnick"]:
305 | self.nickmap[user["ircnick"]] = name
306 |
307 | socket.setdefaulttimeout(timeout)
308 |
309 | def _get_person_by_username(self, irc, username):
310 | """looks up a user by the username"""
311 | if self.registryValue("use_fasjson"):
312 | try:
313 | person = self.fasjsonclient.get_user(username=username).result
314 | except fasjson_client.errors.APIError as e:
315 | if e.code == 404:
316 | irc.reply(f"Sorry, but user '{username}' does not exist")
317 | return
318 | else:
319 | irc.reply("Something blew up, please try again")
320 | self.log.error(e)
321 | return
322 | else:
323 | try:
324 | person = self.fasclient.person_by_username(username)
325 | # convert to the newer FASJSON way for now
326 | if person.get("email"):
327 | person["emails"] = [person["email"]]
328 | except Exception as e:
329 | irc.reply("Something blew up, please try again")
330 | self.log.error(e)
331 | return
332 | if not person:
333 | irc.reply(f"Sorry, but user '{username}' does not exist")
334 | return
335 |
336 | return person
337 |
338 | def refresh(self, irc, msg, args):
339 | """takes no arguments
340 |
341 | Refresh the necessary caches."""
342 |
343 | irc.reply("Downloading caches. This could take a while...")
344 | self._refresh()
345 | irc.replySuccess()
346 |
347 | refresh = wrap(refresh)
348 |
349 | @property
350 | def karma_db_path(self):
351 | return self.registryValue("karma.db_path")
352 |
353 | @property
354 | def allow_unaddressed_karma(self):
355 | return self.registryValue("karma.unaddressed")
356 |
357 | @property
358 | def allow_negative(self):
359 | return self.registryValue("karma.allow_negative")
360 |
361 | def _load_json(self, url):
362 | timeout = socket.getdefaulttimeout()
363 | socket.setdefaulttimeout(45)
364 | try:
365 | json = simplejson.loads(utils.web.getUrl(url))
366 | finally:
367 | socket.setdefaulttimeout(timeout)
368 | return json
369 |
370 | def pulls(self, irc, msg, args, slug):
371 | """
372 |
373 | List the latest pending pull requests on github/pagure repos.
374 | """
375 |
376 | slug = slug.strip()
377 | if not slug or slug.count("/") != 0:
378 | irc.reply("Must be a GitHub org/username or pagure tag")
379 | return
380 |
381 | irc.reply("One moment, please... Looking up %s." % slug)
382 | fail_on_github, fail_on_pagure = False, False
383 | github_repos, pagure_repos = [], []
384 | try:
385 | github_repos = list(self.yield_github_repos(slug))
386 | except IOError as e:
387 | self.log.exception(e.message)
388 | fail_on_github = True
389 |
390 | try:
391 | pagure_repos = list(self.yield_pagure_repos(slug))
392 | except IOError as e:
393 | self.log.exception(e.message)
394 | fail_on_pagure = True
395 |
396 | if fail_on_github and fail_on_pagure:
397 | irc.reply("Could not find %s on GitHub or pagure.io" % slug)
398 | return
399 |
400 | results = sum(
401 | [list(self.yield_github_pulls(slug, r)) for r in github_repos], []
402 | ) + sum([list(self.yield_pagure_pulls(slug, r)) for r in pagure_repos], [])
403 |
404 | # Reverse-sort by time (newest-first)
405 | comparator = lambda a, b: cmp(b["age_numeric"], a["age_numeric"]) # noqa: E731
406 | results.sort(comparator)
407 |
408 | if not results:
409 | irc.reply("No pending pull requests on {slug}".format(slug=slug))
410 | else:
411 | n = 6 # Show 6 pull requests
412 | for pull in results[:n]:
413 | irc.reply(
414 | '@{user}\'s "{title}" {url} filed {age}'.format(
415 | user=pull["user"],
416 | title=pull["title"],
417 | url=pull["url"],
418 | age=pull["age"],
419 | ).encode("utf-8")
420 | )
421 |
422 | if len(results) > n:
423 | irc.reply("... and %i more." % (len(results) - n))
424 |
425 | pulls = wrap(pulls, ["text"])
426 |
427 | def yield_github_repos(self, username):
428 | self.log.info("Finding github repos for %r" % username)
429 | tmpl = "https://api.github.com/users/{username}/repos?per_page=100"
430 | url = tmpl.format(username=username)
431 | auth = dict(access_token=self.github_oauth_token)
432 | for result in self.yield_github_results(url, auth):
433 | yield result["name"]
434 |
435 | def yield_github_pulls(self, username, repo):
436 | self.log.info("Finding github pull requests for %r %r" % (username, repo))
437 | tmpl = "https://api.github.com/repos/{username}/{repo}/pulls?per_page=100"
438 | url = tmpl.format(username=username, repo=repo)
439 | auth = dict(access_token=self.github_oauth_token)
440 | for result in self.yield_github_results(url, auth):
441 | yield dict(
442 | user=result["user"]["login"],
443 | title=result["title"],
444 | url=result["html_url"],
445 | age=arrow.get(result["created_at"]).humanize(),
446 | age_numeric=arrow.get(result["created_at"]),
447 | )
448 |
449 | def yield_github_results(self, url, auth):
450 | results = []
451 | link = dict(next=url)
452 | while "next" in link:
453 | response = requests.get(link["next"], params=auth)
454 |
455 | if response.status_code == 404:
456 | raise IOError("404 for %r" % link["next"])
457 |
458 | # And.. if we didn't get good results, just bail.
459 | if response.status_code != 200:
460 | raise IOError(
461 | "Non-200 status code %r; %r; %r"
462 | % (response.status_code, link["next"], response.json)
463 | )
464 |
465 | results = response.json()
466 |
467 | for result in results:
468 | yield result
469 |
470 | field = response.headers.get("link", None)
471 |
472 | link = dict()
473 | if field:
474 | link = dict(
475 | [
476 | (
477 | part.split("; ")[1][5:-1],
478 | part.split("; ")[0][1:-1],
479 | )
480 | for part in field.split(", ")
481 | ]
482 | )
483 |
484 | def yield_pagure_repos(self, tag):
485 | self.log.info("Finding pagure repos for %r" % tag)
486 | tmpl = "https://pagure.io/api/0/projects?tags={tag}"
487 | url = tmpl.format(tag=tag)
488 | for result in self.yield_pagure_results(url, "projects"):
489 | yield result["name"]
490 |
491 | def yield_pagure_pulls(self, tag, repo):
492 | self.log.info("Finding pagure pull requests for %r %r" % (tag, repo))
493 | tmpl = "https://pagure.io/api/0/{repo}/pull-requests"
494 | url = tmpl.format(tag=tag, repo=repo)
495 | for result in self.yield_pagure_results(url, "requests"):
496 | yield dict(
497 | user=result["user"]["name"],
498 | title=result["title"],
499 | url="https://pagure.io/{repo}/pull-request/{id}".format(
500 | repo=result["project"]["name"], id=result["id"]
501 | ),
502 | age=arrow.get(result["date_created"]).humanize(),
503 | age_numeric=arrow.get(result["date_created"]),
504 | )
505 |
506 | def yield_pagure_results(self, url, key):
507 | response = requests.get(url)
508 |
509 | if response.status_code == 404:
510 | raise IOError("404 for %r" % url)
511 |
512 | # And.. if we didn't get good results, just bail.
513 | if response.status_code != 200:
514 | raise IOError(
515 | "Non-200 status code %r; %r; %r"
516 | % (response.status_code, url, response.text)
517 | )
518 |
519 | results = response.json()
520 | results = results[key]
521 |
522 | for result in results:
523 | yield result
524 |
525 | def whoowns(self, irc, msg, args, package):
526 | """
527 |
528 | Retrieve the owner of a given package
529 | """
530 | # First use pagure info
531 | url = "https://src.fedoraproject.org/api/0/rpms/"
532 | req = requests.get(url + package)
533 | if req.status_code == 404:
534 | irc.reply("Package %s not found." % package)
535 | return
536 |
537 | req_json = req.json()
538 | admins = ", ".join(req_json["access_users"]["admin"])
539 | owners = ", ".join(req_json["access_users"]["owner"])
540 | committers = ", ".join(req_json["access_users"]["commit"])
541 |
542 | if owners:
543 | owners = ircutils.bold("owner: ") + owners
544 | if admins:
545 | admins = ircutils.bold("admin: ") + admins
546 | if committers:
547 | committers = ircutils.bold("commit: ") + committers
548 |
549 | resp = "; ".join([x for x in [owners, admins, committers] if x != ""])
550 |
551 | # Then try using fedora-scm-requests for more info
552 | url = "https://pagure.io/releng/fedora-scm-requests/raw/master/f/rpms/"
553 | req = requests.get(url + package)
554 | if req.status_code == 200:
555 | try:
556 | yml = yaml.load(req.text)
557 | if "bugzilla_contact" in yml:
558 | lines = []
559 | for k, v in yml["bugzilla_contact"].items():
560 | lines.append("%s: %s" % (ircutils.bold(k), v))
561 | resp += " - " + "; ".join(lines)
562 | except yaml.scanner.ScannerError:
563 | # If we can't parse the YAML for some reason, don't worry about
564 | # it. Just return the initial response.
565 | pass
566 | irc.reply(resp)
567 |
568 | whoowns = wrap(whoowns, ["text"])
569 |
570 | def wiki(self, irc, msg, args, page_name):
571 | """
572 |
573 | Return the Fedora wiki link for the specified page."""
574 | link = "https://fedoraproject.org/wiki/{}".format(page_name)
575 |
576 | # Properly format spaces for the wiki link.
577 | link.replace(" ", "_")
578 | irc.reply(link)
579 |
580 | wiki = wrap(wiki, ["text"])
581 |
582 | def what(self, irc, msg, args, package):
583 | """
584 |
585 | Returns a description of a given package.
586 | """
587 | url = "https://apps.fedoraproject.org/mdapi/rawhide/srcpkg/"
588 | req = requests.get(url + package)
589 | if req.status_code == 404:
590 | irc.reply("No such package exists.")
591 | else:
592 | irc.reply("%s: %s" % (package, req.json()["summary"]))
593 |
594 | what = wrap(what, ["text"])
595 |
596 | def fas(self, irc, msg, args, find_name):
597 | """
598 |
599 | Search the Fedora Account System usernames, full names, and email
600 | addresses for a match."""
601 | find_name = to_unicode(find_name)
602 | matches = []
603 | for entry in self.faslist:
604 | if entry.find(find_name.lower()) != -1:
605 | matches.append(entry)
606 | if len(matches) == 0:
607 | irc.reply("'%s' Not Found!" % find_name)
608 | else:
609 | output = []
610 | for match in matches:
611 | output.append(self.faslist[match])
612 | irc.reply(" - ".join(output).encode("utf-8"))
613 |
614 | fas = wrap(fas, ["text"])
615 |
616 | def hellomynameis(self, irc, msg, args, name):
617 | """
618 |
619 | Return brief information about a Fedora Account System username. Useful
620 | for things like meeting roll call and calling attention to yourself."""
621 |
622 | person = self._get_person_by_username(irc, name)
623 |
624 | if not person:
625 | return
626 |
627 | irc.reply(
628 | f"{person['username']} '{person['human_name']}' <{person['emails'][0]}>"
629 | )
630 |
631 | hellomynameis = wrap(hellomynameis, ["text"])
632 |
633 | def himynameis(self, irc, msg, args, name):
634 | """
635 |
636 | Will the real Slim Shady please stand up?"""
637 |
638 | person = self._get_person_by_username(irc, name)
639 |
640 | if not person:
641 | return
642 |
643 | irc.reply(f"{person['username']} 'Slim Shady' <{person['emails'][0]}>")
644 |
645 | himynameis = wrap(himynameis, ["text"])
646 |
647 | def dctime(self, irc, msg, args, dcname):
648 | """
649 |
650 | Returns the current time of the datacenter identified by dcname.
651 | Supported DCs: PHX2, RDU, AMS, osuosl, ibiblio."""
652 | timezone_name = ""
653 | dcname_lower = dcname.lower()
654 | if dcname_lower == "phx2":
655 | timezone_name = "US/Arizona"
656 | elif dcname_lower in ["rdu", "ibiblio"]:
657 | timezone_name = "US/Eastern"
658 | elif dcname_lower == "osuosl":
659 | timezone_name = "US/Pacific"
660 | elif dcname_lower in ["ams", "internetx"]:
661 | timezone_name = "Europe/Amsterdam"
662 | else:
663 | irc.reply("Datacenter %s is unknown" % dcname)
664 | return
665 | try:
666 | time = datetime.datetime.now(pytz.timezone(timezone_name))
667 | except Exception:
668 | irc.reply(
669 | 'The timezone of "%s" was unknown: "%s"' % (dcname, timezone_name)
670 | )
671 | return
672 | irc.reply(
673 | 'The current local time of "%s" is: "%s" (timezone: %s)'
674 | % (dcname, time.strftime("%H:%M"), timezone_name)
675 | )
676 |
677 | dctime = wrap(dctime, ["text"])
678 |
679 | def localtime(self, irc, msg, args, name):
680 | """
681 |
682 | Returns the current time of the user.
683 | The timezone is queried from FAS."""
684 | if name in ["zod", "zodbot"]:
685 | irc.reply("There is no time! Kneel before zod!")
686 | return
687 |
688 | person = self._get_person_by_username(irc, name)
689 | if not person:
690 | return
691 |
692 | timezone_name = person["timezone"]
693 | if timezone_name is None:
694 | irc.reply('User "%s" doesn\'t share their timezone' % name)
695 | return
696 | try:
697 | time = datetime.datetime.now(pytz.timezone(timezone_name))
698 | except Exception:
699 | irc.reply('The timezone of "%s" was unknown: "%s"' % (name, timezone_name))
700 | return
701 | irc.reply(
702 | 'The current local time of "%s" is: "%s" (timezone: %s)'
703 | % (name, time.strftime("%H:%M"), timezone_name)
704 | )
705 |
706 | localtime = wrap(localtime, ["text"])
707 |
708 | def fasinfo(self, irc, msg, args, name):
709 | """
710 |
711 | Return information on a Fedora Account System username."""
712 |
713 | person = self._get_person_by_username(irc, name)
714 | if not person:
715 | return
716 |
717 | if self.registryValue("use_fasjson"):
718 | nicks = get_ircnicks(person)
719 | irc.reply(
720 | f"User: {person.get('username')}, "
721 | f"Name: {person.get('human_name')}, "
722 | f"Email: {' and '.join(e for e in person['emails'] or ['None'])}, "
723 | f"Creation: {person.get('creation')}, "
724 | f"IRC Nicks: {' and '.join(n for n in nicks or ['None'])}, "
725 | f"Timezone: {person.get('timezone')}, "
726 | f"Locale: {person.get('locale')}, "
727 | f"GPG Key IDs: {' and '.join(k for k in person['gpgkeyids'] or ['None'])}, "
728 | f"Status: {person.get('status')}"
729 | )
730 |
731 | groups = self.fasjsonclient.list_user_groups(username=name).result
732 | irc.reply(f"Groups: {', '.join(g['groupname'] for g in groups)}")
733 | else:
734 | # groups and stuff are different in fasjson, so leave
735 | # the FAS stuff here til we are ready
736 | person["creation"] = person["creation"].split(" ")[0]
737 | string = (
738 | "User: %(username)s, Name: %(human_name)s"
739 | ", email: %(email)s, Creation: %(creation)s"
740 | ", IRC Nick: %(ircnick)s, Timezone: %(timezone)s"
741 | ", Locale: %(locale)s"
742 | ", GPG key ID: %(gpg_keyid)s, Status: %(status)s"
743 | ) % person
744 | irc.reply(string.encode("utf-8"))
745 |
746 | # List of unapproved groups is easy
747 | unapproved = ""
748 | for group in person["unapproved_memberships"]:
749 | unapproved = unapproved + "%s " % group["name"]
750 | if unapproved != "":
751 | irc.reply("Unapproved Groups: %s" % unapproved)
752 |
753 | # List of approved groups requires a separate query to extract roles
754 | constraints = {"username": name, "group": "%", "role_status": "approved"}
755 | columns = ["username", "group", "role_type"]
756 | roles = []
757 | try:
758 | roles = self.fasclient.people_query(
759 | constraints=constraints, columns=columns
760 | )
761 | except Exception:
762 | irc.reply("Error getting group memberships.")
763 | return
764 |
765 | approved = ""
766 | for role in roles:
767 | if role["role_type"] == "sponsor":
768 | approved += "+" + role["group"] + " "
769 | elif role["role_type"] == "administrator":
770 | approved += "@" + role["group"] + " "
771 | else:
772 | approved += role["group"] + " "
773 | if approved == "":
774 | approved = "None"
775 |
776 | irc.reply("Approved Groups: %s" % approved)
777 |
778 | fasinfo = wrap(fasinfo, ["text"])
779 |
780 | def group(self, irc, msg, args, name):
781 | """
782 |
783 | Return information about a Fedora Account System group."""
784 |
785 | if self.registryValue("use_fasjson"):
786 | try:
787 | group = self.fasjsonclient.get_group(groupname=name).result
788 | except fasjson_client.errors.APIError as e:
789 | if e.code == 404:
790 | irc.reply(f"Sorry, but group '{name}' does not exist")
791 | return
792 | else:
793 | irc.reply("Something blew up, please try again")
794 | self.log.error(e)
795 | return
796 |
797 | irc.reply(f"{group['groupname']}: {group['description']}")
798 | else:
799 | try:
800 | group = self.fasclient.group_by_name(name)
801 | irc.reply("%s: %s" % (name, group["display_name"]))
802 | except AppError:
803 | irc.reply('There is no group "%s".' % name)
804 |
805 | group = wrap(group, ["text"])
806 |
807 | def admins(self, irc, msg, args, name):
808 | """
809 |
810 | Return the administrators list for the selected group"""
811 | if self.registryValue("use_fasjson"):
812 | irc.reply("Groups no longer have admins. try the 'sponsors' command ")
813 | else:
814 | try:
815 | group = self.fasclient.group_members(name)
816 | sponsors = ""
817 | for person in group:
818 | if person["role_type"] == "administrator":
819 | sponsors += person["username"] + " "
820 | irc.reply("Administrators for %s: %s" % (name, sponsors))
821 | except AppError:
822 | irc.reply("There is no group %s." % name)
823 |
824 | admins = wrap(admins, ["text"])
825 |
826 | def sponsors(self, irc, msg, args, name):
827 | """
828 |
829 | Return the sponsors list for the selected group"""
830 |
831 | if self.registryValue("use_fasjson"):
832 | try:
833 | sponsors = self.fasjsonclient.list_group_sponsors(groupname=name).result
834 | except fasjson_client.errors.APIError as e:
835 | if e.code == 404:
836 | irc.reply(f"Sorry, but group '{name}' does not exist")
837 | return
838 | else:
839 | irc.reply("Something blew up, please try again")
840 | self.log.error(e)
841 | return
842 |
843 | irc.reply(
844 | f"Sponsors for {name}: {', '.join(s['username'] for s in sponsors)}"
845 | )
846 | else:
847 | try:
848 | group = self.fasclient.group_members(name)
849 | sponsors = ""
850 | for person in group:
851 | if person["role_type"] == "sponsor":
852 | sponsors += person["username"] + " "
853 | elif person["role_type"] == "administrator":
854 | sponsors += "@" + person["username"] + " "
855 | irc.reply("Sponsors for %s: %s" % (name, sponsors))
856 | except AppError:
857 | irc.reply("There is no group %s." % name)
858 |
859 | sponsors = wrap(sponsors, ["text"])
860 |
861 | def members(self, irc, msg, args, name):
862 | """
863 |
864 | Return a list of members of the specified group"""
865 | if self.registryValue("use_fasjson"):
866 | try:
867 | members = self.fasjsonclient.list_group_members(groupname=name).result
868 | except fasjson_client.errors.APIError as e:
869 | if e.code == 404:
870 | irc.reply(f"Sorry, but group '{name}' does not exist")
871 | return
872 | else:
873 | irc.reply("Something blew up, please try again")
874 | self.log.error(e)
875 | return
876 |
877 | irc.reply(f"Members of {name}: {', '.join(m['username'] for m in members)}")
878 | else:
879 | try:
880 | group = self.fasclient.group_members(name)
881 | members = ""
882 | for person in group:
883 | if person["role_type"] == "administrator":
884 | members += "@" + person["username"] + " "
885 | elif person["role_type"] == "sponsor":
886 | members += "+" + person["username"] + " "
887 | else:
888 | members += person["username"] + " "
889 | irc.reply("Members of %s: %s" % (name, members))
890 | except AppError:
891 | irc.reply("There is no group %s." % name)
892 |
893 | members = wrap(members, ["text"])
894 |
895 | def showticket(self, irc, msg, args, baseurl, number):
896 | """
897 |
898 | Return the name and URL of a trac ticket or bugzilla bug.
899 | """
900 | url = format(baseurl, str(number))
901 | size = conf.supybot.protocols.http.peekSize()
902 | text = utils.web.getUrl(url, size=size)
903 | parser = Title()
904 | try:
905 | parser.feed(text.decode())
906 | except sgmllib.SGMLParseError:
907 | irc.reply(format("Encountered a problem parsing %u", url))
908 | if parser.title:
909 | irc.reply(utils.web.htmlToText(parser.title.strip()) + " - " + url)
910 | else:
911 | irc.reply(
912 | format(
913 | "That URL appears to have no HTML title "
914 | + "within the first %i bytes.",
915 | size,
916 | )
917 | )
918 |
919 | showticket = wrap(showticket, ["httpUrl", "int"])
920 |
921 | def swedish(self, irc, msg, args):
922 | """takes no arguments
923 |
924 | Humor mmcgrath."""
925 |
926 | # Import this here to avoid a circular import problem.
927 | from . import __version__
928 |
929 | irc.reply(str("kwack kwack"))
930 | irc.reply(str("bork bork bork"))
931 | irc.reply(str("(supybot-fedora version %s)" % __version__))
932 |
933 | swedish = wrap(swedish)
934 |
935 | def invalidCommand(self, irc, msg, tokens):
936 | """Handle any command not otherwise handled.
937 |
938 | We use this to accept karma commands directly.
939 | """
940 | channel = msg.args[0]
941 | if not irc.isChannel(channel):
942 | return
943 |
944 | agent = msg.nick
945 | line = tokens[-1].strip()
946 | words = line.split()
947 | for word in words:
948 | if word[-2:] in self.karma_tokens:
949 | self._do_karma(irc, channel, agent, word, line, explicit=True)
950 |
951 | def doPrivmsg(self, irc, msg):
952 | """Handle everything.
953 |
954 | The name is misleading. This hook actually gets called for all
955 | IRC activity in every channel.
956 | """
957 | # We don't handle this if we've been addressed because invalidCommand
958 | # will handle it for us. This prevents us from accessing the db twice
959 | # and therefore crashing.
960 | if msg.addressed or msg.repliedTo:
961 | return
962 |
963 | channel = msg.args[0]
964 | if irc.isChannel(channel) and self.allow_unaddressed_karma:
965 | irc = callbacks.SimpleProxy(irc, msg)
966 | agent = msg.nick
967 | line = msg.args[1].strip()
968 |
969 | # First try to handle karma commands
970 | words = line.split()
971 | for word in words:
972 | if word[-2:] in self.karma_tokens:
973 | self._do_karma(irc, channel, agent, word, line, explicit=False)
974 |
975 | blacklist = self.registryValue("naked_ping_channel_blacklist")
976 | if irc.isChannel(channel) and channel not in blacklist:
977 | # Also, handle naked pings for
978 | # https://github.com/fedora-infra/supybot-fedora/issues/26
979 | pattern = r"\w* ?[:,] ?ping\W*$"
980 | if re.match(pattern, line):
981 | admonition = self.registryValue("naked_ping_admonition")
982 | irc.reply(admonition)
983 |
984 | def get_current_release(self):
985 | url = (
986 | "https://pdc.fedoraproject.org/rest_api/v1/releases/"
987 | "?active=true&name=Fedora&release_type=ga&fields=version"
988 | "&ordering=version"
989 | )
990 | response = requests.get(url)
991 | data = response.json()
992 | return "f" + str(
993 | max(
994 | [
995 | int(x["version"])
996 | for x in data["results"]
997 | if x["version"] != "Rawhide"
998 | ]
999 | )
1000 | )
1001 |
1002 | def open_karma_db(self):
1003 | data = shelve.open(self.karma_db_path)
1004 | if "backwards" in data:
1005 | # This is the old style data. convert it to the new form.
1006 | release = self.get_current_release()
1007 | data["forwards-" + release] = copy.copy(data["forwards"])
1008 | data["backwards-" + release] = copy.copy(data["backwards"])
1009 | del data["forwards"]
1010 | del data["backwards"]
1011 | data.sync()
1012 | return data
1013 |
1014 | def karma(self, irc, msg, args, name):
1015 | """
1016 |
1017 | Return the total karma for a FAS user."""
1018 | data = None
1019 | try:
1020 | data = self.open_karma_db()
1021 | if name in self.nickmap:
1022 | name = self.nickmap[name]
1023 | current_release = self.get_current_release()
1024 | votes = data["backwards-" + current_release].get(name, {})
1025 | alltime = []
1026 | for key in data:
1027 | if "backwards-" not in key:
1028 | continue
1029 | alltime.append(data[key].get(name, {}))
1030 | finally:
1031 | if data:
1032 | data.close()
1033 |
1034 | inc = len([v for v in votes.values() if v == 1])
1035 | dec = len([v for v in votes.values() if v == -1])
1036 | total = inc - dec
1037 |
1038 | alltime_inc = alltime_dec = 0
1039 | for release in alltime:
1040 | alltime_inc += len([v for v in release.values() if v == 1])
1041 | alltime_dec += len([v for v in release.values() if v == -1])
1042 | alltime_total = alltime_inc - alltime_dec
1043 |
1044 | irc.reply(
1045 | "Karma for %s has been increased %i times and "
1046 | "decreased %i times for release cycle %s for a "
1047 | "total of %i (%i all time)"
1048 | % (name, inc, dec, current_release, total, alltime_total)
1049 | )
1050 |
1051 | karma = wrap(karma, ["text"])
1052 |
1053 | def _do_karma(self, irc, channel, agent, recip, line, explicit=False):
1054 | recip, direction = recip[:-2], recip[-2:]
1055 | if not recip:
1056 | return
1057 |
1058 | # Extract 'puiterwijk' out of 'have a cookie puiterwijk++'
1059 | recip = recip.strip().split()[-1]
1060 |
1061 | # Exclude 'c++', 'g++' or 'i++' (c,g,i), issue #30
1062 | if str(recip).lower() in ["c", "g", "i"]:
1063 | return
1064 |
1065 | increment = direction == "++" # If not, then it must be decrement
1066 |
1067 | # Check that these are FAS users
1068 | if agent not in self.nickmap and agent not in self.users:
1069 | self.log.info("Saw %s from %s, but %s not in FAS" % (recip, agent, agent))
1070 | if explicit:
1071 | irc.reply("Couldn't find %s in FAS" % agent)
1072 | return
1073 |
1074 | if recip not in self.nickmap and recip not in self.users:
1075 | self.log.info("Saw %s from %s, but %s not in FAS" % (recip, agent, recip))
1076 | if explicit:
1077 | irc.reply("Couldn't find %s in FAS" % recip)
1078 | return
1079 |
1080 | # Transform irc nicks into fas usernames if possible.
1081 | if agent in self.nickmap:
1082 | agent = self.nickmap[agent]
1083 |
1084 | if recip in self.nickmap:
1085 | recip = self.nickmap[recip]
1086 |
1087 | if agent == recip:
1088 | irc.reply("You may not modify your own karma.")
1089 | return
1090 |
1091 | release = self.get_current_release()
1092 |
1093 | # Check our karma db to make sure this hasn't already been done.
1094 | data = None
1095 | try:
1096 | data = shelve.open(self.karma_db_path)
1097 | fkey = "forwards-" + release
1098 | bkey = "backwards-" + release
1099 | if fkey not in data:
1100 | data[fkey] = {}
1101 |
1102 | if bkey not in data:
1103 | data[bkey] = {}
1104 |
1105 | if agent not in data[fkey]:
1106 | forwards = data[fkey]
1107 | forwards[agent] = {}
1108 | data[fkey] = forwards
1109 |
1110 | if recip not in data[bkey]:
1111 | backwards = data[bkey]
1112 | backwards[recip] = {}
1113 | data[bkey] = backwards
1114 |
1115 | vote = 1 if increment else -1
1116 |
1117 | if data[fkey][agent].get(recip) == vote:
1118 | # People found this response annoying.
1119 | # https://github.com/fedora-infra/supybot-fedora/issues/25
1120 | # irc.reply(
1121 | # "You have already given %i karma to %s" % (vote, recip))
1122 | return
1123 |
1124 | forwards = data[fkey]
1125 | forwards[agent][recip] = vote
1126 | data[fkey] = forwards
1127 |
1128 | backwards = data[bkey]
1129 | backwards[recip][agent] = vote
1130 | data[bkey] = backwards
1131 |
1132 | # Count the number of karmas for old so-and-so.
1133 | total_this_release = sum(data[bkey][recip].values())
1134 |
1135 | total_all_time = 0
1136 | for key in data:
1137 | if "backwards-" not in key:
1138 | continue
1139 | total_all_time += sum(data[key].get(recip, {}).values())
1140 | finally:
1141 | if data:
1142 | data.close()
1143 |
1144 | # fedmsg.publish(
1145 | # name="supybot.%s" % socket.gethostname(),
1146 | # modname="irc", topic="karma",
1147 | # msg={
1148 | # 'agent': agent,
1149 | # 'recipient': recip,
1150 | # 'total': total_all_time, # The badge rules use this value
1151 | # 'total_this_release': total_this_release,
1152 | # 'vote': vote,
1153 | # 'channel': channel,
1154 | # 'line': line,
1155 | # 'release': release,
1156 | # },
1157 | # )
1158 |
1159 | url = self.registryValue("karma.url")
1160 | irc.reply(
1161 | "Karma for %s changed to %r "
1162 | "(for the release cycle %s): %s"
1163 | % (recip, total_this_release, release, url)
1164 | )
1165 |
1166 | def wikilink(self, irc, msg, args, name):
1167 | """
1168 |
1169 | Return MediaWiki link syntax for a FAS user's page on the wiki."""
1170 |
1171 | person = self._get_person_by_username(irc, name)
1172 | if not person:
1173 | return
1174 |
1175 | string = "[[User:%s|%s]]" % (person["username"], person["human_name"] or "")
1176 | irc.reply(string.encode("utf-8"))
1177 |
1178 | wikilink = wrap(wikilink, ["text"])
1179 |
1180 | def mirroradmins(self, irc, msg, args, hostname):
1181 | """
1182 |
1183 | Return MirrorManager list of FAS usernames which administer .
1184 | must be the FQDN of the host."""
1185 | url = (
1186 | "https://admin.fedoraproject.org/mirrormanager/api/"
1187 | "mirroradmins?name=" + hostname
1188 | )
1189 | result = self._load_json(url)
1190 | if "admins" not in result:
1191 | irc.reply(result.get("message", "Something went wrong"))
1192 | return
1193 | string = "Mirror Admins of %s: " % hostname
1194 | string += " ".join(result["admins"])
1195 | irc.reply(string.encode("utf-8"))
1196 |
1197 | mirroradmins = wrap(mirroradmins, ["text"])
1198 |
1199 | def pushduty(self, irc, msg, args):
1200 | """
1201 |
1202 | Return the list of people who are on releng push duty right now.
1203 | """
1204 |
1205 | def get_persons():
1206 | for meeting in self._meetings_for("release-engineering"):
1207 | yield meeting["meeting_name"]
1208 |
1209 | persons = list(get_persons())
1210 |
1211 | url = f"{self.fedocal_url}release-engineering/"
1212 |
1213 | if not persons:
1214 | response = "Nobody is listed as being on push duty right now..."
1215 | irc.reply(response.encode("utf-8"))
1216 | irc.reply("- " + url.encode("utf-8"))
1217 | return
1218 |
1219 | persons = ", ".join(persons)
1220 | response = "The following people are on push duty: %s" % persons
1221 | irc.reply(response.encode("utf-8"))
1222 | irc.reply(f"- {url}")
1223 |
1224 | pushduty = wrap(pushduty)
1225 |
1226 | def vacation(self, irc, msg, args):
1227 | """
1228 |
1229 | Return the list of people who are on vacation right now.
1230 | """
1231 |
1232 | def get_persons():
1233 | for meeting in self._meetings_for("vacation"):
1234 | for manager in meeting["meeting_manager"]:
1235 | yield manager
1236 |
1237 | persons = list(get_persons())
1238 |
1239 | if not persons:
1240 | response = "Nobody is listed as being on vacation right now..."
1241 | irc.reply(response.encode("utf-8"))
1242 | url = f"{self.fedocal_url}vacation/"
1243 | irc.reply(f"- {url}")
1244 | return
1245 |
1246 | persons = ", ".join(persons)
1247 | response = "The following people are on vacation: %s" % persons
1248 | irc.reply(response.encode("utf-8"))
1249 | url = f"{self.fedocal_url}vacation/"
1250 | irc.reply(f"- {url}")
1251 |
1252 | vacation = wrap(vacation)
1253 |
1254 | def nextmeetings(self, irc, msg, args):
1255 | """
1256 | Return the next meetings scheduled for any channel(s).
1257 | """
1258 | irc.reply("One moment, please... Looking up the channel list.")
1259 | url = f"{self.fedocal_url}api/locations/"
1260 | locations = requests.get(url).json()["locations"]
1261 | self.log.error(f"{locations}")
1262 | meetings = sorted(
1263 | chain(
1264 | *[
1265 | self._future_meetings(location)
1266 | for location in locations
1267 | if "irc.libera.chat" in location
1268 | ]
1269 | ),
1270 | key=itemgetter(0),
1271 | )
1272 |
1273 | if not meetings:
1274 | response = "There are no meetings scheduled at all."
1275 | irc.reply(response.encode("utf-8"))
1276 | return
1277 |
1278 | for date, meeting in meetings[:5]:
1279 | response = "In #%s is %s (starting %s)" % (
1280 | meeting["meeting_location"].split("@")[0].strip(),
1281 | meeting["meeting_name"],
1282 | arrow.get(date).humanize(),
1283 | )
1284 | irc.reply(response.encode("utf-8"))
1285 |
1286 | nextmeetings = wrap(nextmeetings, [])
1287 |
1288 | def nextmeeting(self, irc, msg, args, channel):
1289 | """
1290 |
1291 | Return the next meeting scheduled for a particular channel.
1292 | """
1293 |
1294 | channel = channel.strip("#").split("@")[0]
1295 | meetings = sorted(self._future_meetings(channel), key=itemgetter(0))
1296 |
1297 | if not meetings:
1298 | response = "There are no meetings scheduled for #%s." % channel
1299 | irc.reply(response.encode("utf-8"))
1300 | return
1301 |
1302 | for date, meeting in meetings[:3]:
1303 | response = "In #%s is %s (starting %s)" % (
1304 | channel,
1305 | meeting["meeting_name"],
1306 | arrow.get(date).humanize(),
1307 | )
1308 | irc.reply(response.encode("utf-8"))
1309 | base = f"{self.fedocal_url}location/"
1310 | url = base + urllib.parse.quote("%s@irc.libera.chat/" % channel)
1311 | irc.reply("- " + url.encode("utf-8"))
1312 |
1313 | nextmeeting = wrap(nextmeeting, ["text"])
1314 |
1315 | def _future_meetings(self, location):
1316 | if not location.endswith("@irc.libera.chat"):
1317 | location = "%s@irc.libera.chat" % location
1318 | meetings = self._query_fedocal(location=location)
1319 | now = datetime.datetime.utcnow()
1320 |
1321 | for meeting in meetings:
1322 | string = "%s %s" % (meeting["meeting_date"], meeting["meeting_time_start"])
1323 | dt = datetime.datetime.strptime(string, "%Y-%m-%d %H:%M:%S")
1324 |
1325 | if now < dt:
1326 | yield dt, meeting
1327 |
1328 | def _meetings_for(self, calendar):
1329 | meetings = self._query_fedocal(calendar=calendar)
1330 | now = datetime.datetime.utcnow()
1331 |
1332 | for meeting in meetings:
1333 | string = "%s %s" % (meeting["meeting_date"], meeting["meeting_time_start"])
1334 | start = datetime.datetime.strptime(string, "%Y-%m-%d %H:%M:%S")
1335 | string = "%s %s" % (
1336 | meeting["meeting_date_end"],
1337 | meeting["meeting_time_stop"],
1338 | )
1339 | end = datetime.datetime.strptime(string, "%Y-%m-%d %H:%M:%S")
1340 |
1341 | if now >= start and now <= end:
1342 | yield meeting
1343 |
1344 | def _query_fedocal(self, **kwargs):
1345 | url = f"{self.fedocal_url}api/meetings"
1346 | return requests.get(url, params=kwargs).json()["meetings"]
1347 |
1348 | def badges(self, irc, msg, args, name):
1349 | """
1350 |
1351 | Return badges statistics about a user.
1352 | """
1353 | url = "https://badges.fedoraproject.org/user/" + name
1354 | d = requests.get(url + "/json").json()
1355 |
1356 | if "error" in d:
1357 | response = d["error"]
1358 | else:
1359 | template = "{name} has unlocked {n} Fedora Badges: {url}"
1360 | n = len(d["assertions"])
1361 | response = template.format(name=name, url=url, n=n)
1362 |
1363 | irc.reply(response.encode("utf-8"))
1364 |
1365 | badges = wrap(badges, ["text"])
1366 |
1367 | def quote(self, irc, msg, args, arguments):
1368 | """ [daily, weekly, monthly, quarterly]
1369 |
1370 | Return some datagrepper statistics on fedmsg categories.
1371 | """
1372 |
1373 | # First, some argument parsing. Supybot should be able to do this for
1374 | # us, but I couldn't figure it out. The supybot.plugins.additional
1375 | # object is the thing to use... except its weird.
1376 | tokens = arguments.split(None, 1)
1377 | if len(tokens) == 1:
1378 | symbol, frame = tokens[0], "daily"
1379 | else:
1380 | symbol, frame = tokens
1381 |
1382 | # Second, build a lookup table for symbols. By default, we'll use the
1383 | # fedmsg category names, take their first 3 characters and uppercase
1384 | # them. That will take things like "wiki" and turn them into "WIK" and
1385 | # "bodhi" and turn them into "BOD". This handles a lot for us. We'll
1386 | # then override those that don't make sense manually here. For
1387 | # instance "fedoratagger" by default would be "FED", but that's no
1388 | # good. We want "TAG".
1389 | # Why all this trouble? Well, as new things get added to the fedmsg
1390 | # bus, we don't want to have keep coming back here and modifying this
1391 | # code. Hopefully this dance will at least partially future-proof us.
1392 | symbols = dict(
1393 | [
1394 | (processor.__name__.lower(), processor.__name__[:3].upper())
1395 | for processor in fedmsg.meta.processors # noqa: F821
1396 | ]
1397 | )
1398 | symbols.update(
1399 | {
1400 | "fedoratagger": "TAG",
1401 | "fedbadges": "BDG",
1402 | "buildsys": "KOJ",
1403 | "pkgdb": "PKG",
1404 | "meetbot": "MTB",
1405 | "planet": "PLN",
1406 | "trac": "TRC",
1407 | "mailman": "MM3",
1408 | }
1409 | )
1410 |
1411 | # Now invert the dict so we can lookup the argued symbol.
1412 | # Yes, this is vulnerable to collisions.
1413 | symbols = dict([(sym, name) for name, sym in symbols.items()])
1414 |
1415 | # These aren't user-facing topics, so drop 'em.
1416 | del symbols["LOG"]
1417 | del symbols["UNH"]
1418 | del symbols["ANN"] # And this one is unused...
1419 |
1420 | key_fmt = lambda d: ", ".join(sorted(d.keys())) # noqa: E731
1421 |
1422 | if symbol not in symbols:
1423 | response = "No such symbol %r. Try one of %s"
1424 | irc.reply((response % (symbol, key_fmt(symbols))).encode("utf-8"))
1425 | return
1426 |
1427 | # Now, build another lookup of our various timeframes.
1428 | frames = dict(
1429 | daily=datetime.timedelta(days=1),
1430 | weekly=datetime.timedelta(days=7),
1431 | monthly=datetime.timedelta(days=30),
1432 | quarterly=datetime.timedelta(days=91),
1433 | )
1434 |
1435 | if frame not in frames:
1436 | response = "No such timeframe %r. Try one of %s"
1437 | irc.reply((response % (frame, key_fmt(frames))).encode("utf-8"))
1438 | return
1439 |
1440 | category = [symbols[symbol]]
1441 |
1442 | t2 = datetime.datetime.utcnow()
1443 | t1 = t2 - frames[frame]
1444 | t0 = t1 - frames[frame]
1445 |
1446 | # Count the number of messages between t0 and t1, and between t1 and t2
1447 | query1 = dict(start=t0, end=t1, category=category)
1448 | query2 = dict(start=t1, end=t2, category=category)
1449 |
1450 | # Do this async for superfast datagrepper queries.
1451 | tpool = ThreadPool()
1452 | batched_values = tpool.map(
1453 | datagrepper_query,
1454 | [
1455 | dict(start=x, end=y, category=category)
1456 | for x, y in Utils.daterange(t1, t2, SPARKLINE_RESOLUTION)
1457 | ]
1458 | + [query1, query2],
1459 | )
1460 |
1461 | count2 = batched_values.pop()
1462 | count1 = batched_values.pop()
1463 |
1464 | # Just rename the results. We'll use the rest for the sparkline.
1465 | sparkline_values = batched_values
1466 |
1467 | yester_phrases = dict(
1468 | daily="yesterday",
1469 | weekly="the week preceding this one",
1470 | monthly="the month preceding this one",
1471 | quarterly="the 3 months preceding these past three months",
1472 | )
1473 | phrases = dict(
1474 | daily="24 hours",
1475 | weekly="week",
1476 | monthly="month",
1477 | quarterly="3 months",
1478 | )
1479 |
1480 | if count1 and count2:
1481 | percent = ((float(count2) / count1) - 1) * 100
1482 | elif not count1 and count2:
1483 | # If the older of the two time periods had zero messages, but there
1484 | # are some in the more current period.. well, that's an infinite
1485 | # percent increase.
1486 | percent = float("inf")
1487 | elif not count1 and not count2:
1488 | # If counts are zero for both periods, then the change is 0%.
1489 | percent = 0
1490 | else:
1491 | # Else, if there were some messages in the old time period, but
1492 | # none in the current... then that's a 100% drop off.
1493 | percent = -100
1494 |
1495 | sign = lambda value: value >= 0 and "+" or "-" # noqa: E731
1496 |
1497 | template = "{sym}, {name} {sign}{percent:.2f}% over {phrase}"
1498 | response = template.format(
1499 | sym=symbol,
1500 | name=symbols[symbol],
1501 | sign=sign(percent),
1502 | percent=abs(percent),
1503 | phrase=yester_phrases[frame],
1504 | )
1505 | irc.reply(response.encode("utf-8"))
1506 |
1507 | # Now, make a graph out of it.
1508 | sparkline = Utils.sparkline(sparkline_values)
1509 |
1510 | template = " {sparkline} ⤆ over {phrase}"
1511 | response = template.format(
1512 | sym=symbol, sparkline=sparkline, phrase=phrases[frame]
1513 | )
1514 | irc.reply(response.encode("utf-8"))
1515 |
1516 | to_utc = lambda t: time.gmtime(time.mktime(t.timetuple())) # noqa: E731
1517 | # And a final line for "x-axis tics"
1518 | t1_fmt = time.strftime("%H:%M UTC %m/%d", to_utc(t1))
1519 | t2_fmt = time.strftime("%H:%M UTC %m/%d", to_utc(t2))
1520 | padding = " " * (SPARKLINE_RESOLUTION - len(t1_fmt) - 3)
1521 | template = " ↑ {t1}{padding}↑ {t2}"
1522 | response = template.format(t1=t1_fmt, t2=t2_fmt, padding=padding)
1523 | irc.reply(response.encode("utf-8"))
1524 |
1525 | quote = wrap(quote, ["text"])
1526 |
1527 |
1528 | class Utils(object):
1529 | """Some handy utils for datagrepper visualization."""
1530 |
1531 | @classmethod
1532 | def sparkline(cls, values):
1533 | bar = "▁▂▃▄▅▆▇█"
1534 | barcount = len(bar) - 1
1535 | values = [float(v) for v in values]
1536 | mn, mx = min(values), max(values)
1537 | extent = mx - mn
1538 |
1539 | if extent == 0:
1540 | indices = [0 for n in values]
1541 | else:
1542 | indices = [int((n - mn) / extent * barcount) for n in values]
1543 |
1544 | unicode_sparkline = "".join([bar[i] for i in indices])
1545 | return unicode_sparkline
1546 |
1547 | @classmethod
1548 | def daterange(cls, start, stop, steps):
1549 | """A generator for stepping through time."""
1550 | delta = (stop - start) / steps
1551 | current = start
1552 | while current + delta <= stop:
1553 | yield current, current + delta
1554 | current += delta
1555 |
1556 |
1557 | Class = Fedora
1558 |
1559 |
1560 | # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:
1561 |
--------------------------------------------------------------------------------