├── .gitignore ├── LICENSE ├── README.md ├── example-ruleset.yml ├── ignoretogether.py ├── logo.png └── tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, William Pitcock 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ignore-together 2 | 3 | ![Logo](logo.png) 4 | 5 | A distributed ignore list engine for IRC. 6 | 7 | The idea is essentially that there are users whose behaviours you'd rather not deal with, 8 | ignore-together allows you to preemptively ignore these users if other people you subscribed 9 | to have already ignored them. This is like the hydra effect, but with the goal of enforcing 10 | good behaviour instead of abusive or anti-social behaviour, by introducing reputation data 11 | to the IRC experience. 12 | 13 | This package contains a specification for ignore-list rulesets and an example implementation 14 | for weechat. 15 | 16 | ## usage (weechat) 17 | 18 | This example installs the script and loads my ruleset into the client. 19 | 20 | /script install ignore-together.py 21 | /ignore-together add kaniini http://turtle.dereferenced.org/~kaniini/ignore-together-rules 22 | -------------------------------------------------------------------------------- /example-ruleset.yml: -------------------------------------------------------------------------------- 1 | # This is an example ignore-together ruleset, rulesets are written in a subset of YAML. 2 | # The top level is the name of the rule. All rules have a "type" field. 3 | # "ignore"-type rules have a set of zero or more "hostmasks" and "accountnames". 4 | # "message"-type rules have a set of regular expressions that are applied to the message. 5 | # A "rationale" field provides an explanation of what the ruleset does, 6 | # so the user may select or deselect the rule in a UI. 7 | 8 | "kaniini": 9 | type: "ignore" 10 | hostmasks: 11 | - "*!*@mouse.dereferenced.org" 12 | - "*!*kaniini@*.tu.ok.cox.net" 13 | accountnames: 14 | - "kaniini" 15 | rationale: "self-important aggrandizing person, thinks rabbits shouldn't be murdered, disembowled and turned into dinner" 16 | 17 | "ramnet": 18 | type: "ignore" 19 | hostmasks: 20 | - "*!*@*.gavlcmta01.gsvltx.tl.dh.suddenlink.net" 21 | rationale: "uses 'well,' too frequently as a sentence starter" 22 | 23 | "Diablo-D3": 24 | type: "ignore" 25 | hostmasks: 26 | - "*!*@exelion.net" 27 | rationale: "too much paleo discussions" 28 | 29 | "paleo diet spam": 30 | type: "message" 31 | patterns: 32 | - ".* paleo diet .*" 33 | rationale: "tired of hearing about the paleo diet" 34 | -------------------------------------------------------------------------------- /ignoretogether.py: -------------------------------------------------------------------------------- 1 | # ignore-together.py - a distributed ignore list engine for IRC. 2 | 3 | from __future__ import print_function 4 | 5 | import os 6 | import sys 7 | import yaml 8 | 9 | weechat_is_fake = False 10 | 11 | try: 12 | import weechat 13 | except: 14 | class FakeWeechat: 15 | def command(self, cmd): 16 | print(cmd) 17 | weechat = FakeWeechat() 18 | weechat_is_fake = True 19 | 20 | 21 | class IgnoreRule: 22 | """An ignore rule. 23 | This provides instrumentation for converting ignore rules into weechat filters. 24 | It handles both types of ignore-together ignore rules. 25 | """ 26 | def __init__(self, ignorelist, rulename, rationale, typename='ignore', hostmasks=[], accountnames=[], patterns=[]): 27 | self.ignorelist = ignorelist 28 | self.rulename = rulename.replace(' ', '_') 29 | self.rationale = rationale 30 | self.typename = typename 31 | self.hostmasks = hostmasks 32 | self.accountnames = accountnames 33 | self.patterns = patterns 34 | 35 | def install(self): 36 | "Install an ignore rule." 37 | subrule_ctr = 0 38 | if self.typename == 'ignore': 39 | for pattern in self.hostmasks: 40 | weechat.command('/filter add ignore-together.{ignorelist}.{rulename}.{ctr} irc.* * {pattern}'.format( 41 | ignorelist=self.ignorelist.name, rulename=self.rulename, ctr=subrule_ctr, pattern=pattern)) 42 | subrule_ctr += 1 43 | # XXX - accountnames 44 | elif self.typename == 'message': 45 | for pattern in self.patterns: 46 | weechat.command('/filter add ignore-together.{ignorelist}.{rulename}.{ctr} irc.* * {pattern}'.format( 47 | ignorelist=self.ignorelist.name, rulename=self.rulename, ctr=subrule_ctr, pattern=pattern)) 48 | subrule_ctr += 1 49 | 50 | def uninstall(self): 51 | "Uninstall an ignore rule." 52 | subrule_ctr = 0 53 | if self.typename == 'ignore': 54 | for pattern in self.hostmasks: 55 | weechat.command('/filter del ignore-together.{ignorelist}.{rulename}.{ctr}'.format( 56 | ignorelist=self.ignorelist.name, rulename=self.rulename, ctr=subrule_ctr)) 57 | subrule_ctr += 1 58 | elif self.typename == 'message': 59 | for pattern in self.patterns: 60 | weechat.command('/filter del ignore-together.{ignorelist}.{rulename}.{ctr}'.format( 61 | ignorelist=self.ignorelist.name, rulename=self.rulename, ctr=subrule_ctr)) 62 | subrule_ctr += 1 63 | 64 | 65 | class IgnoreRuleSet: 66 | """A downloaded collection of rules. 67 | Handles merging updates vs current state, and so on.""" 68 | def __init__(self, name, uri): 69 | self.name = name 70 | self.uri = uri 71 | self.rules = [] 72 | 73 | def load(self): 74 | def build_rules(s): 75 | for k, v in s.items(): 76 | self.rules.append(IgnoreRule(self, k, v.get('rationale', '???'), v.get('type', 'ignore'), v.get('hostmasks', []), v.get('accountnames', []), v.get('patterns', []))) 77 | def test_load_cb(payload): 78 | build_rules(yaml.load(payload)) 79 | if weechat_is_fake: 80 | d = open(self.uri, 'r') 81 | return test_load_cb(d.read()) 82 | 83 | def install(self): 84 | [r.install() for r in self.rules] 85 | 86 | def uninstall(self): 87 | [r.uninstall() for r in self.rules] 88 | 89 | 90 | rules = {} 91 | 92 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaniini/ignore-together/38525f83e305caa704fc6f1659e2155f9f5e8ef1/logo.png -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | from ignoretogether import IgnoreRuleSet 4 | 5 | if __name__ == '__main__': 6 | print('Simulation of commands which would be run on example-ruleset.') 7 | rs = IgnoreRuleSet('example', 'example-ruleset.yml') 8 | rs.load() 9 | 10 | print('Ruleset installation.') 11 | rs.install() 12 | 13 | print('Ruleset uninstallation.') 14 | rs.uninstall() 15 | 16 | --------------------------------------------------------------------------------