├── devel └── ansible │ ├── roles │ └── dev │ │ ├── files │ │ ├── supybot │ │ │ ├── data │ │ │ │ └── web │ │ │ │ │ ├── robots.txt.example │ │ │ │ │ ├── generic │ │ │ │ │ └── error.html.example │ │ │ │ │ ├── index.html.example │ │ │ │ │ └── default.css.example │ │ │ └── supybot.conf │ │ ├── ngircd.conf │ │ ├── supybot.service │ │ ├── gssproxy-supybot.conf │ │ └── .bashrc │ │ └── tasks │ │ └── main.yml │ └── playbook.yml ├── .gitignore ├── TODO.txt ├── .coveragerc ├── Makefile ├── tox.ini ├── Vagrantfile ├── .github └── workflows │ └── tests.yml ├── pyproject.toml ├── README.md ├── .cico.pipeline └── supybot_fedora ├── __init__.py ├── test.py ├── config.py └── plugin.py /devel/ansible/roles/dev/files/supybot/data/web/robots.txt.example: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /devel/ansible/playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | become: true 4 | become_method: sudo 5 | roles: 6 | - dev 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | *.pyo 4 | *.swp 5 | dist 6 | .tox 7 | .coverage 8 | conf 9 | logs 10 | *.egg-info/ 11 | .vagrant/ 12 | test-conf/ 13 | test-logs/ 14 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | - fix ext and fasinfo to report whether a Fedora Talk extension is enabled or 2 | disabled (not currently possible, the config setting is only readable by 3 | that user, not anybody) 4 | - function to query the last time the cache was refreshed 5 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | 2 | [run] 3 | branch = True 4 | source = supybot_fedora 5 | 6 | [paths] 7 | source = supybot_fedora 8 | 9 | [report] 10 | fail_under = 15 11 | show_missing = true 12 | exclude_lines = 13 | pragma: no cover 14 | omit = 15 | supybot_fedora/test.py 16 | -------------------------------------------------------------------------------- /devel/ansible/roles/dev/files/ngircd.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Basic ngircd config for supybot-fedora testing 3 | # 4 | 5 | [Global] 6 | Name = irc.supybot.test 7 | Info = supybot-fedora test IRC server 8 | ServerGID = ngircd 9 | ServerUID = ngircd 10 | 11 | [Limits] 12 | MaxNickLength = 24 13 | 14 | [SSL] 15 | CipherList = @SYSTEM 16 | -------------------------------------------------------------------------------- /devel/ansible/roles/dev/files/supybot/data/web/generic/error.html.example: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %(title)s 6 | 7 | 8 | 9 |

Error

10 |

%(error)s

11 | 12 | 13 | -------------------------------------------------------------------------------- /devel/ansible/roles/dev/files/supybot.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=supybot 3 | After=network-online.target 4 | Wants=network-online.target 5 | 6 | [Service] 7 | Environment=GSS_USE_PROXY=yes 8 | User=vagrant 9 | WorkingDirectory=/vagrant 10 | ExecStart=poetry run supybot /home/vagrant/supybot/supybot.conf 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /devel/ansible/roles/dev/files/gssproxy-supybot.conf: -------------------------------------------------------------------------------- 1 | # 2 | # /etc/gssproxy/99-supybot.conf 3 | # 4 | 5 | [service/supybot] 6 | mechs = krb5 7 | cred_store = keytab:/var/lib/gssproxy/supybot.keytab 8 | cred_store = client_keytab:/var/lib/gssproxy/supybot.keytab 9 | allow_constrained_delegation = true 10 | allow_client_ccache_sync = true 11 | cred_usage = both 12 | euid = vagrant 13 | 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION = 0.5.3 2 | 3 | .PHONY: dist 4 | 5 | dist: 6 | mkdir -p dist 7 | mkdir -p supybot-fedora-$(VERSION) 8 | cp *.py *.txt supybot-fedora-$(VERSION) 9 | tar cj supybot-fedora-$(VERSION) > dist/supybot-fedora-$(VERSION).tar.bz2 10 | rm -rf supybot-fedora-$(VERSION) 11 | 12 | upload: 13 | scp dist/supybot-fedora-$(VERSION).tar.bz2 $(BODHI_USER)@fedorahosted.org:/srv/web/releases/s/u/supybot-fedora/. 14 | 15 | clean: 16 | rm -rf dist 17 | -------------------------------------------------------------------------------- /devel/ansible/roles/dev/files/.bashrc: -------------------------------------------------------------------------------- 1 | # .bashrc 2 | 3 | alias fedora-supybot-start="sudo systemctl start supybot.service; sudo systemctl status supybot.service" 4 | alias fedora-supybot-logs="sudo journalctl -u supybot.service" 5 | alias fedora-supybot-restart="sudo systemctl restart supybot.service; sudo systemctl status supybot.service" 6 | alias fedora-supybot-stop="sudo systemctl stop supybot.service; sudo systemctl status supybot.service" 7 | 8 | cd /vagrant 9 | -------------------------------------------------------------------------------- /devel/ansible/roles/dev/files/supybot/data/web/index.html.example: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Supybot Web server index 6 | 7 | 8 | 9 |

Supybot web server index

10 |

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 | --------------------------------------------------------------------------------