├── matrixbot ├── plugins │ ├── __init__.py │ ├── broadcast.py │ ├── feeder.py │ └── trac.py ├── __init__.py ├── utils.py ├── ldap.py └── matrix.py ├── MANIFEST.in ├── README.md ├── .gitignore ├── LICENSE ├── tools ├── matrix-subscriber └── matrix-bot ├── setup.py └── cfg └── matrix-bot.cfg.example /matrixbot/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | recursive-include cfg * 3 | -------------------------------------------------------------------------------- /matrixbot/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __version__ = "0.15.1" 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # matrix-bot 2 | 3 | TODO: Add a summary and the explanation of this project 4 | 5 | TODO: Add examples of usage 6 | 7 | TODO: PEP8 and Pyflake revision 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ---> Python 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | Copyright (c) 2016 Pablo Saavedra 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /tools/matrix-subscriber: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author: Pablo Saavedra 5 | # Maintainer: Pablo Saavedra 6 | # Contact: saavedra.pablo at gmail.com 7 | 8 | import argparse 9 | import traceback 10 | import time 11 | 12 | from matrixbot import utils 13 | from matrixbot import matrix 14 | 15 | ## vars ######################################################################## 16 | conffile = ".matrixbot.cfg" 17 | 18 | ## command line options parser ################################################# 19 | parser = argparse.ArgumentParser() 20 | parser.add_argument("-c", "--conffile", dest="conffile", default=conffile, 21 | help="Conffile (default: %s)" % conffile) 22 | args = parser.parse_args() 23 | conffile = args.conffile 24 | 25 | # setting up ################################################################### 26 | settings = utils.get_default_settings() 27 | utils.setup(conffile, settings) 28 | logger = utils.create_logger(settings) 29 | 30 | ## main #################################################################### 31 | if __name__ == '__main__': 32 | try: 33 | m = matrix.MatrixBot(settings) 34 | m.join_rooms(silent=True) 35 | m.invite_subscriptions() 36 | m.kick_revokations() 37 | except Exception, e: 38 | logger.error("Unexpected error: %s" % e) 39 | logger.error("Unexpected error: %s" % traceback.print_exc()) 40 | -------------------------------------------------------------------------------- /tools/matrix-bot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author: Pablo Saavedra 5 | # Maintainer: Pablo Saavedra 6 | # Contact: saavedra.pablo at gmail.com 7 | 8 | import argparse 9 | import traceback 10 | import time 11 | 12 | from matrixbot import utils 13 | from matrixbot import matrix 14 | 15 | ## vars ######################################################################## 16 | conffile = ".matrixbot.cfg" 17 | 18 | ## command line options parser ################################################# 19 | parser = argparse.ArgumentParser() 20 | parser.add_argument("-c", "--conffile", dest="conffile", default=conffile, 21 | help="Conffile (default: %s)" % conffile) 22 | args = parser.parse_args() 23 | conffile = args.conffile 24 | 25 | # setting up ################################################################### 26 | settings = utils.get_default_settings() 27 | utils.setup(conffile, settings) 28 | logger = utils.create_logger(settings) 29 | 30 | ## main ######################################################################## 31 | if __name__ == '__main__': 32 | while True: 33 | try: 34 | m = matrix.MatrixBot(settings) 35 | m.join_rooms(silent=True) 36 | m.sync(ignore=True) # Ignoring pending old messages 37 | while True: 38 | m.sync() 39 | except Exception, e: 40 | logger.error("Unexpected error: %s" % e) 41 | logger.error("Unexpected error: %s" % traceback.print_exc()) 42 | time.sleep(10) 43 | -------------------------------------------------------------------------------- /matrixbot/plugins/broadcast.py: -------------------------------------------------------------------------------- 1 | from matrixbot import utils 2 | 3 | class BroadcastPlugin: 4 | def __init__(self, bot, settings): 5 | self.logger = utils.get_logger() 6 | self.bot = bot 7 | self.settings = settings 8 | self.logger.info("BroadcastPlugin loaded (%(name)s)" % settings) 9 | 10 | def async(self, handler): 11 | return 12 | 13 | def command(self, sender, room_id, body, handler): 14 | self.logger.debug("BroadcastPlugin command") 15 | plugin_name = self.settings["name"] 16 | 17 | command_list = body.split()[1:] 18 | 19 | if len(command_list) > 0 and command_list[0] == plugin_name: 20 | if sender not in self.settings["users"]: 21 | self.logger.warning("User %s not autorized to use BroadcastPlugin" % self) 22 | return 23 | announcement = body[body.find(plugin_name) + len(plugin_name) + 1:] 24 | html = "
%s" % ('Announcement:', announcement) 25 | for room_id in self.settings["rooms"]: 26 | room_id = self.bot.get_real_room_id(room_id) 27 | self.logger.debug( 28 | "BroadcastPlugin announcement in %s: %s" % ( 29 | room_id, announcement 30 | ) 31 | ) 32 | self.bot.send_html(room_id,html) 33 | 34 | def help(self, sender, room_id, handler): 35 | self.logger.debug("BroadcastPlugin help") 36 | if sender in self.settings["users"]: 37 | if self.bot.is_private_room(room_id, self.bot.get_user_id()): 38 | message = "%(name)s Announcement to be sent\n" % self.settings 39 | else: 40 | message = "%(username)s: %(name)s Announcement to be sent\n" % self.settings 41 | handler(room_id, message) 42 | 43 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import os 3 | 4 | here = os.path.abspath(os.path.dirname(__file__)) 5 | 6 | 7 | def read_file(path_segments): 8 | """Read a file from the package. Takes a list of strings to join to 9 | make the path""" 10 | file_path = os.path.join(here, *path_segments) 11 | with open(file_path) as f: 12 | return f.read() 13 | 14 | 15 | def exec_file(path_segments): 16 | """Execute a single python file to get 17 | the variables defined in it""" 18 | result = {} 19 | code = read_file(path_segments) 20 | exec(code, result) 21 | return result 22 | 23 | version = exec_file(("matrixbot", "__init__.py"))["__version__"] 24 | 25 | long_description = "" 26 | try: 27 | long_description = file('README.md').read() 28 | except Exception: 29 | pass 30 | 31 | license = "" 32 | try: 33 | license = file('LICENSE').read() 34 | except Exception: 35 | pass 36 | 37 | 38 | setup( 39 | name='matrix-bot', 40 | version=version, 41 | description='A matrix.org bot', 42 | author='Pablo Saavedra', 43 | author_email='saavedra.pablo@gmail.com', 44 | url='http://github.com/psaavedra/matrix-bot', 45 | packages=find_packages(), 46 | package_data={ 47 | "matrixbot": [ "../cfg/matrix-bot.cfg.example" ] 48 | }, 49 | scripts=[ 50 | "tools/matrix-bot", 51 | "tools/matrix-subscriber", 52 | ], 53 | zip_safe=False, 54 | install_requires=[ 55 | "getconf", 56 | "matrix-client>=0.0.6", 57 | "python-ldap", 58 | "python-memcached", 59 | "feedparser", 60 | "pytz", 61 | ], 62 | 63 | download_url='https://github.com/psaavedra/matrix-bot/zipball/master', 64 | classifiers=[ 65 | "Development Status :: 2 - Pre-Alpha", 66 | "License :: OSI Approved :: MIT License", 67 | "Programming Language :: Python", 68 | "Topic :: Communications :: Chat", 69 | ], 70 | long_description=long_description, 71 | license=license, 72 | keywords="python matrix bot matrix-python-sdk", 73 | ) 74 | -------------------------------------------------------------------------------- /matrixbot/plugins/feeder.py: -------------------------------------------------------------------------------- 1 | import feedparser 2 | import pytz 3 | from datetime import datetime, timedelta 4 | from matrixbot import utils 5 | from dateutil import parser 6 | 7 | def utcnow(): 8 | now = datetime.utcnow() 9 | return now.replace(tzinfo=pytz.utc) 10 | 11 | class FeederPlugin: 12 | def __init__(self, bot, settings): 13 | self.logger = utils.get_logger() 14 | self.bot = bot 15 | self.settings = settings 16 | self.logger.info("FeederPlugin loaded (%(name)s)" % settings) 17 | self.timestamp = {} 18 | for feed in self.settings["feeds"].keys(): 19 | self.timestamp[feed] = utcnow() 20 | 21 | def pretty_entry(self, entry): 22 | entry["title"] 23 | entry["author"] 24 | entry["link"] 25 | res = """%(title)s by %(author)s (%(link)s)""" % entry 26 | return res 27 | 28 | def async(self, handler): 29 | self.logger.debug("FeederPlugin async") 30 | 31 | res = [] 32 | for feed_name, feed_url in self.settings["feeds"].iteritems(): 33 | self.logger.debug("FeederPlugin async: Fetching %s ..." % feed_name) 34 | try: 35 | feed = feedparser.parse(feed_url) 36 | updated = feed['feed']['updated'] 37 | updated_dt = parser.parse(updated) 38 | if updated_dt > self.timestamp[feed_name]: 39 | for entry in feed['entries']: 40 | entry_dt = parser.parse(entry["updated"]) 41 | if entry_dt > self.timestamp[feed_name]: 42 | res.append(entry) 43 | self.timestamp[feed_name] = updated_dt 44 | except Exception as e: 45 | self.logger.error("FeederPlugin got error in feed %s: %s" % (feed_name,e)) 46 | 47 | if len(res) == 0: 48 | return 49 | 50 | res = map( 51 | self.pretty_entry, 52 | res 53 | ) 54 | message = "\n".join(res) 55 | for room_id in self.settings["rooms"]: 56 | room_id = self.bot.get_real_room_id(room_id) 57 | self.bot.send_notice(room_id, message) 58 | 59 | def command(self, sender, room_id, body, handler): 60 | self.logger.debug("FeederPlugin command") 61 | return 62 | 63 | def help(self, sender, room_id, handler): 64 | self.logger.debug("FeederPlugin help") 65 | return 66 | -------------------------------------------------------------------------------- /cfg/matrix-bot.cfg.example: -------------------------------------------------------------------------------- 1 | settings["DEFAULT"] = { 2 | "loglevel": 10, 3 | "logfile": "/dev/stdout", 4 | "period": 30, 5 | } 6 | settings["matrix"] = { 7 | "uri": "http://localhost:8000", 8 | "username": "username", 9 | "password": "password", 10 | "domain": "matrix.org", 11 | "rooms": [] 12 | } 13 | settings["memcached"] = { 14 | "ip": "127.0.0.1", 15 | "port": 11211, 16 | "timeout": 300, 17 | } 18 | settings["ldap"] = { 19 | "server": "ldap://ldap.local", 20 | "base": "ou=People,dc=example,dc=com", 21 | "groups": [], 22 | "groups_id": "cn", 23 | "groups_filter": "(objectClass=posixGroup)", 24 | "groups_base": "ou=Group,dc=example,dc=com", 25 | "users_aliases": { 26 | "user1":"username1", 27 | }, 28 | } 29 | settings["aliases"] = { 30 | "simple_invite":"invite +group1 +group2", 31 | "simple_kick":"invite +group1 +group2", 32 | } 33 | settings["subscriptions"] = { 34 | "#room_alias1":"@user1 @user2 but @user3", 35 | "#room_alias2":"+group1 +group2 but @user1", 36 | } 37 | settings["revokations"] = { 38 | "#room_alias1":"@user1 @user2 but @user3", 39 | "#room_alias2":"+group1 +group2 but @user1", 40 | } 41 | settings["allowed-join"] = { 42 | "default": "+group1 +group2 but @user1", 43 | "#room_alias1": "+group1 +group2", 44 | } 45 | 46 | 47 | # settings["plugins"] = {} 48 | 49 | # plugin_trac={} 50 | # plugin_trac["module"] = "matrixbot.plugins.trac" 51 | # plugin_trac["class"] = "TracPlugin" 52 | # plugin_trac["settings"] = { 53 | # "username": "username", 54 | # "name": "trac", 55 | # "rooms": ["!room_id"], 56 | # "url_protocol": "https", 57 | # "url_domain": "trac", 58 | # "url_path": "/tracker", 59 | # "url_auth_user": "user", 60 | # "url_auth_password": "password", 61 | # "status": ['new', 'reopened', 'closed'], 62 | # } 63 | # 64 | # settings["plugins"]["plugin_trac"] = plugin_trac 65 | 66 | # plugin_broadcast={} 67 | # plugin_broadcast["module"] = "matrixbot.plugins.broadcast" 68 | # plugin_broadcast["class"] = "BroadcastPlugin" 69 | # plugin_broadcast["settings"] = { 70 | # "username": "username", 71 | # "name": "broadcast", 72 | # "users": ["@user1"], 73 | # "rooms": ["!room_id", 74 | # "#room_alias"], 75 | # } 76 | # 77 | # settings["plugins"]["plugin_broadcast"] = plugin_broadcast 78 | 79 | # plugin_feeder={} 80 | # plugin_feeder["module"] = "matrixbot.plugins.feeder" 81 | # plugin_feeder["class"] = "FeederPlugin" 82 | # plugin_feeder["settings"] = { 83 | # "username": "username", 84 | # "name": "feed", 85 | # "rooms": ["!room_id"], 86 | # "feeds": { 87 | # "matrix-bot": "https://github.com/psaavedra/matrix-bot/commits/master.atom", 88 | # }, 89 | # } 90 | # 91 | # settings["plugins"]["plugin_feeder"] = plugin_feeder 92 | -------------------------------------------------------------------------------- /matrixbot/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # 4 | # Author: Pablo Saavedra 5 | # Maintainer: Pablo Saavedra 6 | # Contact: saavedra.pablo at gmail.com 7 | 8 | import sys 9 | import logging 10 | import copy 11 | import memcache 12 | 13 | def get_default_settings(): 14 | settings = {} 15 | settings["DEFAULT"] = { 16 | "loglevel": 10, 17 | "logfile": "/dev/stdout", 18 | "period": 30, 19 | } 20 | settings["memcached"] = { 21 | "ip": "127.0.0.1", 22 | "port": 11211, 23 | "timeout": 300, 24 | } 25 | settings["matrix"] = { 26 | "uri": "http://localhost:8000", 27 | "username": "username", 28 | "password": "password", 29 | "domain": "matrix.org", 30 | "rooms": [], 31 | "only_local_domain": False, 32 | } 33 | settings["ldap"] = { 34 | "server": "ldap://ldap.local", 35 | "base": "ou=People,dc=example,dc=com", 36 | "groups": [], 37 | "groups_id": "cn", 38 | "groups_filter": "(objectClass=posixGroup)", 39 | "groups_base": "ou=Group,dc=example,dc=com", 40 | "users_aliases": {}, 41 | } 42 | settings["aliases"] = { 43 | } 44 | settings["subscriptions"] = { 45 | } 46 | settings["revokations"] = { 47 | } 48 | settings["allowed-join"] = { 49 | "default": "" 50 | } 51 | settings["plugins"] = {} 52 | return settings 53 | 54 | 55 | def debug_conffile(settings, logger): 56 | for s in settings.keys(): 57 | for k in settings[s].keys(): 58 | key = "%s.%s" % (s, k) 59 | value = settings[s][k] 60 | logger.debug("Configuration setting - %s: %s" % (key, value)) 61 | 62 | 63 | def setup(conffile, settings): 64 | try: 65 | reload(sys) 66 | # Forcing UTF-8 in the enviroment: 67 | sys.setdefaultencoding('utf-8') 68 | # http://stackoverflow.com/questions/3828723/why-we-need-sys-setdefaultencodingutf-8-in-a-py-scrip 69 | except Exception: 70 | pass 71 | execfile(conffile) 72 | 73 | 74 | def create_cache(settings): 75 | cache = memcache.Client(['%(ip)s:%(port)s' % settings["memcached"]], debug=0) 76 | return cache 77 | 78 | def create_logger(settings): 79 | hdlr = logging.FileHandler(settings["DEFAULT"]["logfile"]) 80 | hdlr.setFormatter(logging.Formatter('%(levelname)s %(asctime)s %(message)s')) 81 | logger = logging.getLogger('matrixbot') 82 | logger.addHandler(hdlr) 83 | logger.setLevel(settings["DEFAULT"]["loglevel"]) 84 | logger.debug("Default encoding: %s" % sys.getdefaultencoding()) 85 | debug_conffile(settings, logger) 86 | return logger 87 | 88 | 89 | def get_logger(): 90 | return logging.getLogger('matrixbot') 91 | 92 | 93 | def get_command_alias(message, settings): 94 | prefix = message.strip().split()[0] 95 | command = " ".join(message.strip().split()[1:]) 96 | if command in settings["aliases"].keys(): 97 | return prefix + " " + settings["aliases"][command] 98 | return message 99 | 100 | 101 | def get_aliases(settings): 102 | res = copy.copy(settings["aliases"]) 103 | return res 104 | -------------------------------------------------------------------------------- /matrixbot/plugins/trac.py: -------------------------------------------------------------------------------- 1 | import xmlrpclib 2 | from datetime import datetime, timedelta 3 | from matrixbot import utils 4 | 5 | class TracPlugin: 6 | def __init__(self, bot, settings): 7 | self.logger = utils.get_logger() 8 | self.bot = bot 9 | self.settings = settings 10 | self.logger.info("TracPlugin loaded (%(name)s)" % settings) 11 | self.timestamp = datetime.utcnow() 12 | self.server = xmlrpclib.ServerProxy( 13 | '%(url_protocol)s://%(url_auth_user)s:%(url_auth_password)s@%(url_domain)s%(url_path)s/login/xmlrpc' % self.settings 14 | ) 15 | 16 | def pretty_ticket(self, ticket): 17 | ticket[3]["ticket_id"] = ticket[0] 18 | url = '%(url_protocol)s://%(url_domain)s%(url_path)s' % self.settings 19 | ticket[3]["ticket_url"] = "%s/ticket/%s" % (url, ticket[0]) 20 | res = """%(summary)s: 21 | * URL: %(ticket_url)s 22 | * [severity: %(severity)s] [owner: %(owner)s] [reporter: %(reporter)s] [status: %(status)s]""" % ticket[3] 23 | return res 24 | 25 | def async(self, handler): 26 | self.logger.debug("TracPlugin async") 27 | server = self.server 28 | 29 | d = self.timestamp 30 | self.timestamp = datetime.utcnow() 31 | res = [] 32 | for t in server.ticket.getRecentChanges(d): 33 | ticket = server.ticket.get(t) 34 | changes = server.ticket.changeLog(t) 35 | if len(changes) == 0 and 'new' in self.settings['status']: # No changes implies New ticket 36 | res.append(ticket) 37 | for c in changes: 38 | if ( 39 | c[0] > d and c[2] == 'status' 40 | and c[4] in self.settings['status'] 41 | ): 42 | res.append(ticket) 43 | 44 | if len(res) == 0: 45 | return 46 | 47 | res = map( 48 | self.pretty_ticket, 49 | res 50 | ) 51 | message = "\n".join(res) 52 | for room_id in self.settings["rooms"]: 53 | room_id = self.bot.get_real_room_id(room_id) 54 | handler(room_id, message) 55 | 56 | def command(self, sender, room_id, body, handler): 57 | self.logger.debug("TracPlugin command") 58 | plugin_name = self.settings["name"] 59 | 60 | # TODO: This should be a decorator 61 | if self.bot.only_local_domain and not self.bot.is_local_user_id(sender): 62 | self.logger.warning( 63 | "TracPlugin %s plugin is not allowed for external sender (%s)" % (plugin_name, sender) 64 | ) 65 | return 66 | 67 | sender = sender.replace('@','') 68 | sender = sender.split(':')[0] 69 | command_list = body.split()[1:] 70 | if len(command_list) > 0 and command_list[0] == plugin_name: 71 | if command_list[1] == "create": 72 | summary = ' '.join(command_list[2:]) 73 | self.logger.debug( 74 | "TracPlugin command: %s(%s)" % ( 75 | "create", summary 76 | ) 77 | ) 78 | self.server.ticket.create( 79 | summary, 80 | "", 81 | {"cc": sender}, 82 | True 83 | ) 84 | 85 | def help(self, sender, room_id, handler): 86 | self.logger.debug("TracPlugin help") 87 | if self.bot.is_private_room(room_id, self.bot.get_user_id()): 88 | message = "%(name)s create This is the issue summary\n" % self.settings 89 | else: 90 | message = "%(username)s: %(name)s create This is the issue summary\n" % self.settings 91 | handler(room_id, message) 92 | -------------------------------------------------------------------------------- /matrixbot/ldap.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | # 3 | # Author: Pablo Saavedra 4 | # Maintainer: Pablo Saavedra 5 | # Contact: saavedra.pablo at gmail.com 6 | 7 | from __future__ import absolute_import 8 | 9 | import ldap as LDAP 10 | 11 | from . import utils 12 | 13 | 14 | def get_custom_ldap_group_members(ldap_settings, group_name): 15 | logger = utils.get_logger() 16 | ldap_server = ldap_settings["server"] 17 | ldap_base = ldap_settings["base"] 18 | get_uid = lambda x: x[1]["uid"][0] 19 | members = [] 20 | try: 21 | conn = LDAP.initialize(ldap_server) 22 | g_ldap_filter = ldap_settings[group_name] 23 | logger.debug("Searching members for %s: %s" % (group_name, 24 | g_ldap_filter)) 25 | items = conn.search_s(ldap_base, LDAP.SCOPE_SUBTREE, 26 | attrlist=['uid'], 27 | filterstr=g_ldap_filter) 28 | members = map(get_uid, items) 29 | except Exception, e: 30 | logger.error("Error getting custom group %s from LDAP: %s" % (group_name, e)) 31 | return members 32 | 33 | 34 | def get_ldap_group_members(ldap_settings, group_name): 35 | # base:dc=example,dc=com 36 | # filter:(&(objectClass=posixGroup)(cn={group_name})) 37 | logger = utils.get_logger() 38 | ldap_server = ldap_settings["server"] 39 | ldap_base = ldap_settings["groups_base"] 40 | ldap_filter = "(&%s(%s={group_name}))" % (ldap_settings["groups_filter"], ldap_settings["groups_id"]) 41 | get_uid = lambda x: x.split(",")[0].split("=")[1] 42 | try: 43 | ad_filter = ldap_filter.replace('{group_name}', group_name) 44 | conn = LDAP.initialize(ldap_server) 45 | logger.debug("Searching members for %s: %s - %s - %s" % (group_name, 46 | ldap_server, 47 | ldap_base, 48 | ad_filter)) 49 | res = conn.search_s(ldap_base, LDAP.SCOPE_SUBTREE, ad_filter) 50 | except Exception, e: 51 | logger.error("Error getting group from LDAP: %s" % e) 52 | 53 | return map(get_uid, res[0][1]['uniqueMember']) 54 | 55 | 56 | def get_ldap_groups(ldap_settings): 57 | '''Returns the a list of found LDAP groups filtered with the groups list in 58 | the settings 59 | ''' 60 | # filter:(objectClass=posixGroup) 61 | # base:ou=Group,dc=example,dc=com 62 | logger = utils.get_logger() 63 | ldap_server = ldap_settings["server"] 64 | ldap_base = ldap_settings["groups_base"] 65 | ldap_filter = ldap_settings["groups_filter"] 66 | ldap_groups = ldap_settings["groups"] 67 | get_uid = lambda x: x[1]["cn"][0] 68 | try: 69 | conn = LDAP.initialize(ldap_server) 70 | logger.debug("Searching groups: %s - %s - %s" % (ldap_server, 71 | ldap_base, 72 | ldap_filter)) 73 | res = conn.search_s(ldap_base, LDAP.SCOPE_SUBTREE, ldap_filter) 74 | return filter((lambda x: x in ldap_groups), map(get_uid, res)) 75 | except Exception, e: 76 | logger.error("Error getting groups from LDAP: %s" % e) 77 | 78 | 79 | def get_ldap_groups_members(ldap_settings): 80 | def map_aliases(x): 81 | return ldap_settings.get('users_aliases', {}).get(x, x) 82 | 83 | ldap_groups = ldap_settings["groups"] 84 | groups = get_ldap_groups(ldap_settings) 85 | res = {} 86 | for g in groups: 87 | res[g] = map(map_aliases, get_ldap_group_members(ldap_settings, g)) 88 | 89 | # pending groups to get members. filters for those groups are explicitelly 90 | # defined in the settings 91 | custom_groups = filter((lambda x: x not in groups), ldap_groups) 92 | for g in custom_groups: 93 | res[g] = map(map_aliases, get_custom_ldap_group_members(ldap_settings, g)) 94 | return res 95 | 96 | 97 | def get_groups(ldap_settings): 98 | return ldap_settings["groups"] 99 | -------------------------------------------------------------------------------- /matrixbot/matrix.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | # 3 | # Author: Pablo Saavedra 4 | # Maintainer: Pablo Saavedra 5 | # Contact: saavedra.pablo at gmail.com 6 | 7 | from matrix_client.api import MatrixHttpApi, MatrixRequestError 8 | from matrix_client.client import MatrixClient 9 | from matrix_client.room import Room 10 | 11 | # import pprint 12 | import time 13 | import re 14 | 15 | from . import utils 16 | from . import ldap as bot_ldap 17 | 18 | 19 | class MatrixBot(): 20 | def __init__(self, settings): 21 | self.sync_token = None 22 | 23 | self.logger = utils.get_logger() 24 | self.cache = utils.create_cache(settings) 25 | self.cache_timeout = int(settings["memcached"]["timeout"]) 26 | 27 | self.settings = settings 28 | self.period = settings["DEFAULT"]["period"] 29 | self.uri = settings["matrix"]["uri"] 30 | self.username = settings["matrix"]["username"].lower() 31 | self.password = settings["matrix"]["password"] 32 | self.room_ids = settings["matrix"]["rooms"] 33 | self.domain = self.settings["matrix"]["domain"] 34 | self.only_local_domain = self.settings["matrix"]["only_local_domain"] 35 | 36 | self.subscriptions_room_ids = settings["subscriptions"].keys() 37 | self.revokations_rooms_ids = settings["revokations"].keys() 38 | self.allowed_join_rooms_ids = filter(lambda x: x != 'default', settings["allowed-join"].keys()) 39 | self.default_allowed_join_rooms = settings["allowed-join"]["default"] 40 | 41 | self.client = MatrixClient(self.uri) 42 | self.token = self.client.login_with_password(username=self.username, 43 | password=self.password) 44 | self.api = MatrixHttpApi(self.uri, token=self.token) 45 | 46 | self.rooms = [] 47 | self.room_aliases = {} 48 | self.plugins = [] 49 | for plugin in settings['plugins'].itervalues(): 50 | mod = __import__(plugin['module'], fromlist=[plugin['class']]) 51 | klass = getattr(mod, plugin['class']) 52 | self.plugins.append(klass(self, plugin['settings'])) 53 | 54 | def _get_selected_users(self, groups_users_list): 55 | def _add_or_remove_user(users, username, append): 56 | username = self.normalize_user_id(username) 57 | if append and username not in users["in"]: 58 | users["in"].append(username) 59 | if not append and username not in users["out"]: 60 | users["out"].append(username) 61 | 62 | ldap_settings = self.settings["ldap"] 63 | append = True 64 | users = { 65 | "in": [], 66 | "out": [] 67 | } 68 | for item in groups_users_list: 69 | if item == ("but"): 70 | append = False 71 | elif item.startswith("+"): 72 | group_name = item[1:] 73 | groups_members = bot_ldap.get_ldap_groups_members(ldap_settings) 74 | if group_name in groups_members.keys(): 75 | map( 76 | lambda x: _add_or_remove_user(users, x, append), 77 | groups_members[group_name]) 78 | else: 79 | _add_or_remove_user(users, item, append) 80 | 81 | selected_users = filter( 82 | lambda x: x not in users["out"], 83 | users["in"]) 84 | return selected_users 85 | 86 | def normalize_user_id(self, user_id): 87 | if not user_id.startswith("@"): 88 | user_id = "@" + user_id 89 | self.logger.debug("Adding missing '@' to the username: %s" % user_id) 90 | if user_id.count(":") == 0: 91 | user_id = "%s:%s" % (user_id, self.domain) 92 | return user_id 93 | 94 | def get_user_id(self, username=None, normalized=True): 95 | if not username: 96 | username = self.username 97 | normalized_username = self.normalize_user_id(username) 98 | if normalized: 99 | return normalized_username 100 | else: 101 | return normalized_username[1:].split(':')[0] 102 | 103 | def is_local_user_id(self, username): 104 | normalized_username = self.get_user_id(username, normalized=True) 105 | if normalized_username.split(':')[1] == self.domain: 106 | return True 107 | return False 108 | 109 | def get_real_room_id(self, room_id): 110 | if room_id.startswith("#"): 111 | room_id = self.api.get_room_id(room_id) 112 | return room_id 113 | 114 | def get_room_members(self, room_id): 115 | key = "get_room_members-%s" % room_id 116 | res = self.cache.get(key) 117 | if res: 118 | self.logger.debug("get_room_members (cached): %s" % (key)) 119 | return res 120 | res = self.call_api("get_room_members", 2, room_id) 121 | self.cache.set(key, res, self.cache_timeout) 122 | self.logger.debug("get_room_members (non cached): %s" % (key)) 123 | return res 124 | 125 | def is_room_member(self, room_id, user_id): 126 | try: 127 | r = Room(self.client, room_id) 128 | return user_id in r.get_joined_members().keys() 129 | except Exception, e: 130 | return False 131 | return False 132 | 133 | def do_command(self, action, sender, room_id, body, attempts=3): 134 | if sender: 135 | sender = self.normalize_user_id(sender) 136 | 137 | # TODO: This should be a decorator 138 | if self.only_local_domain and not self.is_local_user_id(sender): 139 | self.logger.warning( 140 | "do_command is not allowed for external sender (%s)" % sender 141 | ) 142 | return 143 | 144 | body_arg_list = body.split()[2:] 145 | dry_mode = False 146 | if ( 147 | len(body_arg_list) > 0 and 148 | body_arg_list[0] == "dryrun" 149 | ): 150 | dry_mode = True 151 | body_arg_list = body.split()[3:] 152 | target_room_id = room_id 153 | if ( 154 | len(body_arg_list) > 0 and 155 | ( 156 | body_arg_list[0].startswith('!') or 157 | body_arg_list[0].startswith('#') 158 | ) 159 | ): 160 | target_room_id = self.get_real_room_id(body_arg_list[0]) 161 | body_arg_list = body_arg_list[1:] 162 | 163 | if sender and not self.is_room_member(target_room_id, sender): 164 | msg = "%s is not allowed for not members (%s) of the room (%s)" % (action, sender, target_room_id) 165 | self.logger.warning(msg) 166 | self.send_private_message(sender, 167 | msg, 168 | room_id) 169 | return 170 | 171 | selected_users = self._get_selected_users(body_arg_list) 172 | 173 | if dry_mode and sender: 174 | self.send_private_message( 175 | sender, 176 | "Simulated '%s' action in room '%s' over: %s" % ( 177 | action, 178 | target_room_id, 179 | " ".join(selected_users)), 180 | room_id) 181 | else: 182 | if len(selected_users) > 0: 183 | for user in selected_users: 184 | self.logger.info( 185 | " do_command (%s,%s,%s,dry_mode=%s)" % ( 186 | action, 187 | target_room_id, 188 | user, 189 | dry_mode)) 190 | res = self.call_api(action, attempts, target_room_id, user) 191 | if sender: 192 | msg = '''Action '%s' in room %s over %s''' % ( 193 | action, 194 | target_room_id, 195 | " ".join(selected_users) 196 | ) 197 | self.send_private_message(sender, msg, room_id) 198 | elif sender: 199 | self.send_private_message(sender, 200 | "No users found", 201 | room_id) 202 | 203 | def invite_subscriptions(self): 204 | for room_id in self.subscriptions_room_ids: 205 | body = "bender: invite " + self.settings["subscriptions"][room_id] 206 | self.do_command("invite_user", None, room_id, body, attempts=1) 207 | 208 | def kick_revokations(self): 209 | for room_id in self.revokations_rooms_ids: 210 | body = "bender: kick " + self.settings["revokations"][room_id] 211 | self.do_command("kick_user", None, room_id, body, attempts=1) 212 | 213 | def call_api(self, action, max_attempts, *args): 214 | method = getattr(self.api, action) 215 | attempts = max_attempts 216 | while attempts > 0: 217 | try: 218 | response = method(*args) 219 | self.logger.info("Call %s action with: %s" % (action, args)) 220 | self.logger.debug("Call response: %s" % (response)) 221 | return response 222 | except MatrixRequestError, e: 223 | self.logger.error("Fail (%s/%s) in call %s action with: %s - %s" % (attempts, max_attempts, action, args, e)) 224 | attempts -= 1 225 | time.sleep(5) 226 | return str(e) 227 | 228 | def send_emote(self, room_id, message): 229 | return self.call_api("send_emote", 3, 230 | room_id, message) 231 | 232 | def send_html(self, room_id, message): 233 | content = { 234 | "body": re.sub('<[^<]+?>', '', message), 235 | "msgtype": "m.text", 236 | "format": "org.matrix.custom.html", 237 | "formatted_body": message 238 | } 239 | return self.api.send_message_event( 240 | room_id, "m.room.message", 241 | content 242 | ) 243 | 244 | def send_message(self, room_id, message): 245 | return self.call_api("send_message", 3, 246 | room_id, message) 247 | 248 | def send_notice(self, room_id, message): 249 | return self.call_api("send_notice", 3, 250 | room_id, message) 251 | 252 | def send_private_message(self, user_id, message, room_id=None): 253 | user_room_id = self.get_private_room_with(user_id) 254 | if room_id and room_id != user_room_id: 255 | self.call_api( 256 | "send_message", 257 | 3, 258 | room_id, 259 | "Replying command as PM to %s" % user_id) 260 | return self.call_api("send_message", 3, 261 | user_room_id, message) 262 | 263 | def leave_empty_rooms(self): 264 | self.logger.debug("leave_empty_rooms") 265 | rooms = self.get_rooms() 266 | for room_id in rooms: 267 | res = self.get_room_members(room_id) 268 | try: 269 | members_list = res.get('chunk', []) 270 | except Exception, e: 271 | members_list = [] 272 | self.logger.debug("Error getting the list of members in room %s: %s" % (room_id, e)) 273 | 274 | if len(members_list) > 2: 275 | continue # We are looking for a 1-to-1 room 276 | 277 | for r in res.get('chunk', []): 278 | if 'user_id' in r and 'membership' in r: 279 | if r['membership'] == 'leave': 280 | self.call_api("kick_user", 1, room_id, self.get_user_id()) 281 | try: 282 | self.call_api("forget_room", 1, room_id) 283 | except Exception, e: 284 | self.logger.warning("Some kind of error during the forget_room action: %s" % (e)) 285 | 286 | def get_private_room_with(self, user_id): 287 | self.leave_empty_rooms() 288 | self.logger.debug("get_private_room_with") 289 | 290 | rooms = self.get_rooms() 291 | for room_id in rooms: 292 | if self.is_private_room(room_id, self.get_user_id(), user_id): 293 | return room_id 294 | 295 | # Not room found then ... 296 | room_id = self.call_api("create_room", 3, 297 | None, False, 298 | [user_id])['room_id'] 299 | self.call_api( 300 | "send_message", 301 | 3, 302 | room_id, 303 | "Hi! Get info about how to interact with me typing: %s help" % self.username 304 | ) 305 | return room_id 306 | 307 | def is_private_room(self, room_id, user1_id, user2_id=None): 308 | me = False # me is true if the user1_id is in the room 309 | him = False # him is true if the user2_id join or is already 310 | 311 | res = self.get_room_members(room_id) 312 | try: 313 | members_list = res.get('chunk', []) 314 | except Exception, e: 315 | members_list = [] 316 | self.logger.debug( 317 | "Error getting the members of the room %s: %s" % (room_id, e)) 318 | 319 | if len(members_list) != 2: 320 | self.logger.debug("Room %s is not a 1-to-1 room" % room_id) 321 | return False # We are looking for a 1-to-1 room 322 | 323 | if not user2_id: 324 | self.logger.debug("Room %s is a 1-to-1 with the user %s" % (room_id, user1_id)) 325 | return True # I just check if the room is 1-to-1 for user1_id 326 | 327 | 328 | # TODO: This code must be cleaned up 329 | for r in res.get('chunk', []): 330 | if 'state_key' in r and 'membership' in r: 331 | if r['state_key'] == user2_id and r['membership'] == 'invite': 332 | him = True 333 | if r['state_key'] == user2_id and r['membership'] == 'join': 334 | him = True 335 | if r['state_key'] == user1_id and r['membership'] == 'join': 336 | me = True 337 | if me and him: 338 | self.logger.debug( 339 | "A 1-to-1 room for %s and %s found: %s" % ( 340 | user2_id, 341 | user1_id, 342 | room_id)) 343 | return True 344 | 345 | for r in res.get('chunk', []): 346 | if ( 347 | 'prev_content' in r 348 | and 'state_key' in r['prev_content'] 349 | and 'membership' in r['prev_content'] 350 | ): 351 | p = r['prev_content'] 352 | if p['state_key'] == user2_id and p['membership'] == 'invite': 353 | him = True 354 | if p['state_key'] == user2_id and p['membership'] == 'join': 355 | him = True 356 | if p['state_key'] == user1_id and p['membership'] == 'join': 357 | me = True 358 | if me and him: 359 | self.logger.debug( 360 | "A 1-to-1 room for %s and %s found: %s" % ( 361 | user2_id, 362 | user1_id, 363 | room_id)) 364 | return True 365 | return False 366 | 367 | def is_explicit_call(self, body): 368 | if ( 369 | body.lower().strip().startswith("%s:" % self.username.lower()) 370 | or body.lower().strip().startswith("%s " % self.username.lower()) 371 | ): 372 | return True 373 | res = False 374 | self.logger.debug("is_explicit_call: %s" % res) 375 | return res 376 | 377 | def is_command(self, body, command="command_name"): 378 | res = False 379 | if self.is_explicit_call(body): 380 | command_list = body.split()[1:] 381 | if len(command_list) == 0: 382 | if command == "help": 383 | res = True 384 | else: 385 | if command_list[0] == command: 386 | res = True 387 | self.logger.debug("is_%s: %s" % (command, res)) 388 | return res 389 | 390 | def join_rooms(self, silent=True): 391 | for room_id in self.room_ids: 392 | try: 393 | room = self.client.join_room(room_id) 394 | room_id = room.room_id # Ensure we are using the actual id not the alias 395 | if not silent: 396 | self.send_message(room_id, "Mornings!") 397 | except MatrixRequestError, e: 398 | self.logger.error("Join action in room %s failed: %s" % 399 | (room_id, e)) 400 | 401 | new_subscriptions_room_ids = [] 402 | for room_id in self.subscriptions_room_ids: 403 | try: 404 | old_room_id = room_id 405 | room_id = room_id + ':' + self.domain 406 | room = self.client.join_room(room_id) 407 | new_room_id = room.room_id # Ensure we are using the actual id not the alias 408 | new_subscriptions_room_ids.append(new_room_id) 409 | self.settings["subscriptions"][new_room_id] = self.settings["subscriptions"][old_room_id] 410 | except MatrixRequestError, e: 411 | self.logger.error("Join action for subscribe users in room %s failed: %s" % 412 | (room_id, e)) 413 | self.subscriptions_room_ids = new_subscriptions_room_ids 414 | 415 | new_revokations_room_ids = [] 416 | for room_id in self.revokations_rooms_ids: 417 | try: 418 | old_room_id = room_id 419 | room_id = room_id + ':' + self.domain 420 | room = self.client.join_room(room_id) 421 | new_room_id = room.room_id # Ensure we are using the actual id not the alias 422 | new_revokations_room_ids.append(new_room_id) 423 | self.settings["revokations"][new_room_id] = self.settings["revokations"][old_room_id] 424 | except MatrixRequestError, e: 425 | self.logger.error("Join action for revoke users in room %s failed: %s" % 426 | (room_id, e)) 427 | self.revokations_rooms_ids = new_revokations_room_ids 428 | 429 | def do_join(self, sender, room_id, body): 430 | self.logger.debug("do_join") 431 | 432 | # TODO: This should be a decorator 433 | if self.only_local_domain and not self.is_local_user_id(sender): 434 | self.logger.warning( 435 | "do_join is not allowed for external sender (%s)" % sender 436 | ) 437 | return 438 | 439 | body_arg_list = body.split()[2:] 440 | dry_mode = False 441 | msg_dry_mode = " (dryrun)" if dry_mode else "" 442 | 443 | if len(body_arg_list) > 0 and body_arg_list[0] == "dryrun": 444 | dry_mode = True 445 | body_arg_list = body.split()[3:] 446 | original_room_id = body_arg_list[0] 447 | join_room_id = body_arg_list[0] 448 | 449 | if not join_room_id.endswith(":%s" % self.domain): 450 | msg = '''Invalid room id (%s): Join is only for rooms in %s domain''' % (join_room_id, self.domain) 451 | self.send_private_message(sender, msg, room_id) 452 | return 453 | 454 | if not join_room_id.startswith("#"): 455 | msg = '''Invalid room id (%s): Join is only valid using room aliases''' % (join_room_id) 456 | self.send_private_message(sender, msg, room_id) 457 | return 458 | 459 | try: 460 | join_room_id = self.get_real_room_id(join_room_id) 461 | except Exception, e: 462 | msg = '''Room not %s found: %s''' % (join_room_id, e) 463 | self.send_private_message(sender, msg, room_id) 464 | self.logger.warning(msg) 465 | return 466 | 467 | allowed_users = self.default_allowed_join_rooms 468 | original_room_name = original_room_id.split(":")[0] 469 | if (original_room_name in self.allowed_join_rooms_ids): 470 | allowed_users = self.settings["allowed-join"][original_room_name] 471 | 472 | selected_users = self._get_selected_users(allowed_users.split()) 473 | 474 | self.logger.debug("Checking if %s is in %s" % (sender, selected_users)) 475 | if sender not in selected_users: 476 | msg = '''User %s can't join in room %s''' % (sender, original_room_id) + msg_dry_mode 477 | self.send_private_message(sender, msg, room_id) 478 | return 479 | 480 | try: 481 | if not dry_mode: 482 | self.logger.info( 483 | "do_join (%s,%s)" % ( 484 | join_room_id, 485 | sender 486 | ) 487 | ) 488 | res = self.call_api("invite_user", 3, join_room_id, sender) 489 | if type(res) == dict: 490 | msg_ok = '''Invitation sent to user %s to join in %s%s''' % ( 491 | sender, 492 | original_room_id, 493 | msg_dry_mode) 494 | self.send_private_message(sender, msg_ok, room_id) 495 | else: 496 | msg_fail = '''Fail in invitation sent to user %s to join in %s%s: %s''' % ( 497 | sender, 498 | original_room_id, 499 | msg_dry_mode, 500 | res) 501 | self.send_private_message(sender, msg_fail, room_id) 502 | except MatrixRequestError, e: 503 | self.logger.warning(e) 504 | 505 | def do_list_groups(self, sender, room_id): 506 | self.logger.debug("do_list_groups") 507 | # TODO: This should be a decorator 508 | if self.only_local_domain and not self.is_local_user_id(sender): 509 | self.logger.warning( 510 | "do_list_groups is not allowed for external sender (%s)" % sender 511 | ) 512 | return 513 | 514 | groups = ', '.join(map( 515 | lambda x: "+%s" % x, 516 | self.settings["ldap"]["groups"] 517 | )) 518 | try: 519 | msg = "Groups: %s" % groups 520 | self.send_private_message(sender, msg, room_id) 521 | except MatrixRequestError, e: 522 | self.logger.warning(e) 523 | 524 | def do_list_rooms(self, sender, room_id): 525 | self.logger.debug("do_list_rooms") 526 | # TODO: This should be a decorator 527 | if self.only_local_domain and not self.is_local_user_id(sender): 528 | self.logger.warning( 529 | "do_list_rooms is not allowed for external sender (%s)" % sender 530 | ) 531 | return 532 | msg = "Room list:\n" 533 | rooms = self.get_rooms() 534 | rooms_msg_list = [] 535 | for r in rooms: 536 | aliases = self.get_room_aliases(r) 537 | if len(aliases) < 1: 538 | self.logger.debug("Room %s hasn't got aliases. Skipping" % (r)) 539 | continue # We are looking for rooms with alias 540 | try: 541 | name = self.api.get_room_name(r)['name'] 542 | except Exception, e: 543 | self.logger.debug("Error getting the room name %s: %s" % (r, e)) 544 | name = "No named" 545 | rooms_msg_list.append("* %s - %s" % (name, " ".join(aliases))) 546 | msg += "\n".join(sorted(rooms_msg_list)) 547 | try: 548 | self.send_private_message(sender, msg, room_id) 549 | except MatrixRequestError, e: 550 | self.logger.warning(e) 551 | 552 | def do_list(self, sender, room_id, body): 553 | self.logger.debug("do_list") 554 | # TODO: This should be a decorator 555 | if self.only_local_domain and not self.is_local_user_id(sender): 556 | self.logger.warning( 557 | "do_list is not allowed for external sender (%s)" % sender 558 | ) 559 | return 560 | 561 | body_arg_list = body.split()[2:] 562 | selected_users = self._get_selected_users(body_arg_list) 563 | msg_list = " ".join( 564 | map(lambda x: self.normalize_user_id(x), selected_users) 565 | ) 566 | try: 567 | self.send_private_message(sender, msg_list, room_id) 568 | except MatrixRequestError, e: 569 | self.logger.warning(e) 570 | 571 | def do_count(self, sender, room_id, body): 572 | # TODO: This should be a decorator 573 | if self.only_local_domain and not self.is_local_user_id(sender): 574 | self.logger.warning( 575 | "do_count is not allowed for external sender (%s)" % sender 576 | ) 577 | return 578 | 579 | self.logger.debug("do_count") 580 | body_arg_list = body.split()[2:] 581 | selected_users = self._get_selected_users(body_arg_list) 582 | msg_list = "Count: %s" % len(selected_users) 583 | try: 584 | self.send_private_message(sender, msg_list, room_id) 585 | except MatrixRequestError, e: 586 | self.logger.warning(e) 587 | 588 | def do_help(self, sender, room_id, body, pm=False): 589 | vars_ = self.settings["matrix"].copy() 590 | if pm: 591 | vars_["prefix"] = "" 592 | else: 593 | vars_["prefix"] = "%(username)s: " % vars_ 594 | 595 | vars_["aliases"] = "\n".join(map(lambda x: "%s: " % vars_["username"] + "%s ==> %s" % x, 596 | utils.get_aliases(self.settings).items())) 597 | try: 598 | self.logger.debug("do_help") 599 | msg_help = '''Examples: 600 | %(prefix)shelp 601 | %(prefix)shelp extra 602 | %(prefix)sjoin