├── MANIFEST.in ├── matrixbot ├── plugins │ ├── __init__.py │ ├── echo.py │ ├── wkbugsfeeder.py │ ├── broadcast.py │ ├── feeder.py │ ├── trac.py │ ├── wktestbotsfeeder.py │ └── wkbotsfeeder.py ├── __init__.py ├── ldap.py ├── utils.py └── matrix.py ├── requirements.txt ├── .github └── workflows │ ├── sync-main-master.yml │ └── close_stale_issues_and_pr.yml ├── cfg ├── echo-test-template.cfg ├── matrix-bot.cfg.example-super-users └── matrix-bot.cfg.example ├── tests └── webkit-plugins-unit-tests.sh ├── install-dependencies.sh ├── .gitignore ├── LICENSE ├── README.md ├── tools ├── matrix-bot ├── matrix-subscriber ├── README.md ├── matrix-echo └── matrix-digest └── setup.py /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | recursive-include cfg * 3 | -------------------------------------------------------------------------------- /matrixbot/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # -*- coding: utf-8 -*- 4 | -------------------------------------------------------------------------------- /matrixbot/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # -*- coding: utf-8 -*- 4 | __version__ = "0.20.2" 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2021.5.30 2 | chardet==3.0.4 3 | charset-normalizer==2.0.4 4 | feedparser==6.0.8 5 | getconf==1.5.1 6 | idna==2.8 7 | matrix-client==0.4.0 8 | pyasn1==0.4.8 9 | pyasn1-modules==0.2.8 10 | python-dateutil==2.8.1 11 | python-ldap==3.1.0 12 | python-memcached==1.59 13 | pytz==2018.9 14 | requests==2.26.0 15 | sgmllib3k==1.0.0 16 | six==1.16.0 17 | urllib3==1.26.5 18 | -------------------------------------------------------------------------------- /.github/workflows/sync-main-master.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - 'main' 5 | 6 | jobs: 7 | mirror_job: 8 | runs-on: ubuntu-latest 9 | name: Mirror 'main' branch to 'master' branch 10 | steps: 11 | - name: Mirror action step 12 | id: mirror 13 | uses: google/mirror-branch-action@v1.0 14 | with: 15 | github-token: ${{ secrets.GITHUB_TOKEN }} 16 | source: 'main' 17 | dest: 'master' 18 | -------------------------------------------------------------------------------- /cfg/echo-test-template.cfg: -------------------------------------------------------------------------------- 1 | settings['DEFAULT'] = { 2 | 'loglevel': 10, 3 | 'logfile': '/dev/stdout', 4 | 'period': 5 5 | } 6 | settings['matrix'] = { 7 | 'uri': 'URI', 8 | 'username': 'USER', 9 | 'password': 'PASSWORD', 10 | 'domain': 'DOMAIN', 11 | 'rooms': ROOMS, 12 | 'only_local_domain': False 13 | } 14 | 15 | echo_plugin = { 16 | 'module': "matrixbot.plugins.echo", 17 | 'class': "EchoPlugin", 18 | 'settings': { 19 | 'message': 'hello room!', 20 | 'username': 'USER', 21 | 'rooms': ROOMS 22 | } 23 | } 24 | 25 | settings['plugins'] = {} 26 | settings['plugins']['echo'] = echo_plugin 27 | -------------------------------------------------------------------------------- /tests/webkit-plugins-unit-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | fatal() { 4 | echo "Error: $1" 5 | exit 1 6 | } 7 | 8 | ROOT=$(git rev-parse --show-toplevel) 9 | 10 | PLUGINS=( 11 | $ROOT/matrixbot/plugins/wkbotsfeeder.py 12 | $ROOT/matrixbot/plugins/wkbugsfeeder.py 13 | $ROOT/matrixbot/plugins/wktestbotsfeeder.py 14 | ) 15 | 16 | which python3 &>/dev/null 17 | if [[ $? -ne 0 ]]; then 18 | fatal "Python3 is required" 19 | fi 20 | 21 | for each in ${PLUGINS[@]}; do 22 | echo -n "Testing $(realpath --relative-to=$ROOT $each): " 23 | content=$(python3 "$each" 2>&1) 24 | if [[ $? -eq 0 ]]; then 25 | echo "OK" 26 | else 27 | echo "Error" 28 | echo "$content" 29 | exit 1 30 | fi 31 | done 32 | -------------------------------------------------------------------------------- /cfg/matrix-bot.cfg.example-super-users: -------------------------------------------------------------------------------- 1 | # 2 | # Minimal configuration for the 'list-rooms' command supporting super-users 3 | # 4 | 5 | settings["DEFAULT"] = { 6 | "loglevel": 10, 7 | "logfile": "/dev/stdout", 8 | "period": 5, 9 | } 10 | 11 | settings["matrix"] = { 12 | "uri": "https://localhost.dummy.net", 13 | "username": "user", 14 | "password": "password", 15 | "domain": "dummy.net", 16 | "only_local_domain": True, 17 | "rooms": [ 18 | "#test:dummy.net" 19 | ], 20 | "super_users" : [ 21 | "@userx:igalia.com", 22 | "@usery:igalia.com" 23 | ], 24 | } 25 | 26 | settings["commands"] = { 27 | "enable": True, 28 | "list-rooms": { 29 | "enable": True, 30 | "visible_subset": [ 31 | "#alpha:igalia.com", 32 | "#beta:igalia.com" 33 | ], 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /install-dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | debian_packages=( 4 | libldap2-dev 5 | libsasl2-dev 6 | libssl-dev 7 | python3-dev 8 | ) 9 | 10 | redhat_packages=( 11 | python3-devel 12 | openldap-devel 13 | ) 14 | 15 | function guess_package_manager() { 16 | # Debian or Ubuntu. 17 | if apt --version &>/dev/null; then 18 | echo "deb" 19 | fi 20 | 21 | # RedHat. 22 | if yum --version &>/dev/null; then 23 | echo "yum" 24 | fi 25 | } 26 | 27 | install_package_dependencies() { 28 | local packageManager=$(guess_package_manager) 29 | 30 | if [[ "$packageManager" == "deb" ]]; then 31 | sudo apt install "${debian_packages[@]}" 32 | elif [[ "$packageManager" == "yum" ]]; then 33 | sudo yum install "${redhat_packages[@]}" 34 | fi 35 | } 36 | 37 | install_module_dependencies() { 38 | pip3 install -r requirements.txt 39 | } 40 | 41 | install_package_dependencies 42 | install_module_dependencies 43 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # matrix-bot 2 | 3 | ## Dependencies 4 | 5 | Run script `install-package-dependencies.sh` to install all the required dependencies. 6 | 7 | Systems supported: Debian, Ubuntu & RedHat. 8 | 9 | ## Install 10 | 11 | ### Installation from PIP 12 | 13 | ``` 14 | $ pip install git+https://github.com/psaavedra/matrix-bot.git 15 | ``` 16 | 17 | ### Installation for development 18 | 19 | ``` 20 | $ git clone http://github.com/psaavedra/matrix-bot.git 21 | $ cd matrix-bot/ 22 | $ ./install-dependencies.sh 23 | $ ln -s $PWD/matrixbot $PWD/tools/matrixbot 24 | $ ln -s tools/matrix-bot 25 | $ cat > test.cfg < 0: 45 | link = " (%s)" % link 46 | res = """%s%s%s%s""" % (title, author, link, status) 47 | return res 48 | -------------------------------------------------------------------------------- /matrixbot/plugins/broadcast.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from matrixbot import utils 4 | 5 | class BroadcastPlugin: 6 | def __init__(self, bot, settings): 7 | self.name = "BroadcastPlugin" 8 | self.logger = utils.get_logger() 9 | self.bot = bot 10 | self.settings = settings 11 | self.logger.info("BroadcastPlugin loaded (%(name)s)" % settings) 12 | 13 | def dispatch(self, handler): 14 | return 15 | 16 | def command(self, sender, room_id, body, handler): 17 | self.logger.debug("BroadcastPlugin command") 18 | plugin_name = self.settings["name"] 19 | 20 | command_list = body.split()[1:] 21 | 22 | if len(command_list) > 0 and command_list[0] == plugin_name: 23 | if sender not in self.settings["users"]: 24 | self.logger.warning("User %s not autorized to use BroadcastPlugin" % self) 25 | return 26 | announcement = body[body.find(plugin_name) + len(plugin_name) + 1:] 27 | html = "

%s

%s
" % ('Announcement:', announcement) 28 | for room_id in self.settings["rooms"]: 29 | room_id = self.bot.get_real_room_id(room_id) 30 | self.logger.debug( 31 | "BroadcastPlugin announcement in %s: %s" % ( 32 | room_id, announcement 33 | ) 34 | ) 35 | self.bot.send_html(room_id,html) 36 | 37 | def help(self, sender, room_id, handler): 38 | self.logger.debug("BroadcastPlugin help") 39 | if sender in self.settings["users"]: 40 | if self.bot.is_private_room(room_id, self.bot.get_user_id()): 41 | message = "%(name)s Announcement to be sent\n" % self.settings 42 | else: 43 | message = "%(username)s: %(name)s Announcement to be sent\n" % self.settings 44 | handler(room_id, message) 45 | 46 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import os 3 | 4 | here = os.path.abspath(os.path.dirname(__file__)) 5 | 6 | try: # for pip >= 10 7 | from pip._internal.req import parse_requirements 8 | except ImportError: # for pip <= 9.0.3 9 | from pip.req import parse_requirements 10 | 11 | def read_file(path_segments): 12 | """Read a file from the package. Takes a list of strings to join to 13 | make the path""" 14 | file_path = os.path.join(here, *path_segments) 15 | with open(file_path) as f: 16 | return f.read() 17 | 18 | 19 | def exec_file(path_segments): 20 | """Execute a single python file to get 21 | the variables defined in it""" 22 | result = {} 23 | code = read_file(path_segments) 24 | exec(code, result) 25 | return result 26 | 27 | version = exec_file(("matrixbot", "__init__.py"))["__version__"] 28 | 29 | long_description = "" 30 | try: 31 | long_description = file('README.md').read() 32 | except Exception: 33 | pass 34 | 35 | license = "" 36 | try: 37 | license = file('LICENSE').read() 38 | except Exception: 39 | pass 40 | 41 | 42 | setup( 43 | name='matrix-bot', 44 | version=version, 45 | description='A matrix.org bot', 46 | author='Pablo Saavedra', 47 | author_email='saavedra.pablo@gmail.com', 48 | url='http://github.com/psaavedra/matrix-bot', 49 | packages=find_packages(), 50 | package_data={ 51 | "matrixbot": [ 52 | "../cfg/matrix-bot.cfg.example", 53 | "../cfg/echo-test-template.cfg" 54 | ] 55 | }, 56 | scripts=[ 57 | "tools/matrix-bot", 58 | "tools/matrix-digest", 59 | "tools/matrix-echo", 60 | "tools/matrix-subscriber", 61 | ], 62 | zip_safe=False, 63 | install_requires = list(map(lambda x:x.requirement,parse_requirements('requirements.txt', session=''))), 64 | 65 | download_url='https://github.com/psaavedra/matrix-bot/zipball/master', 66 | classifiers=[ 67 | "Development Status :: 2 - Pre-Alpha", 68 | "License :: OSI Approved :: MIT License", 69 | "Programming Language :: Python", 70 | "Topic :: Communications :: Chat", 71 | ], 72 | long_description=long_description, 73 | license=license, 74 | keywords="python matrix bot matrix-python-sdk", 75 | ) 76 | -------------------------------------------------------------------------------- /tools/matrix-echo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | usage() { 4 | local program_name=$(basename "$0") 5 | echo "Usage: $program_name [CONFIG-FILE] | [URI] [ROOM..]" 6 | exit $1 7 | } 8 | 9 | use_git_credentials() { 10 | if [[ -f "$HOME/.git-credentials" ]]; then 11 | line=$(grep -m1 "$DOMAIN" ~/.git-credentials) 12 | if [[ "$?" -eq "0" ]]; then 13 | credentials=$(echo "$line" | sed -r 's|https?://(\w+):(\w+)@[[:alnum:]\.]+|\1:\2|g') 14 | USER=$(echo "$credentials" | cut -d ":" -f 1) 15 | PASSWORD=$(echo "$credentials" | cut -d ":" -f 2) 16 | fi 17 | fi 18 | } 19 | 20 | scan_args() { 21 | if [[ "$#" -eq 0 ]]; then 22 | usage 1 23 | fi 24 | 25 | ROOMS=() 26 | while [[ $# -gt 0 ]]; do 27 | if [[ "$1" == "-u" || "$1" == "--user" ]]; then 28 | shift 29 | USER="$1" 30 | elif [[ "$1" == "-p" || "$1" == "--password" ]]; then 31 | shift 32 | PASSWORD="$1" 33 | elif [[ "$1" == "-i" || "$1" == "--uri" ]]; then 34 | shift 35 | URI="$1" 36 | elif [[ "$1" == "-d" || "$1" == "--domain" ]]; then 37 | shift 38 | DOMAIN="$1" 39 | elif [[ "$1" == "-r" || "$1" == "--room" ]]; then 40 | shift 41 | ROOMS+=("'$1'") 42 | else 43 | ARGS+=("$1") 44 | fi 45 | shift 46 | done 47 | if [[ -n $ARGS ]]; then 48 | if [[ "${#ARGS[@]}" -ne 1 ]]; then 49 | usage 1 50 | fi 51 | else 52 | if [[ -z "$URI" || -z "$ROOMS" ]]; then 53 | usage 1 54 | fi 55 | fi 56 | if [[ -n "$URI" && -z "$DOMAIN" ]]; then 57 | DOMAIN=$(echo "$URI" | awk -F. '{printf("%s.%s", $(NF-1),$NF)}') 58 | fi 59 | if [[ -z "$USER" || -z "$PASSWORD" ]]; then 60 | use_git_credentials 61 | fi 62 | if [[ -n "$ROOMS" ]]; then 63 | ROOMS=$(IFS=, printf "%s", ${ROOMS[@]}) 64 | ROOMS=${ROOMS::-1} 65 | ROOMS="[$ROOMS]" 66 | fi 67 | } 68 | 69 | cleanup() { 70 | sleep 1 71 | rm -f "$CONFIG" 2>/dev/null 72 | } 73 | 74 | ROOT=$(git rev-parse --show-toplevel) 75 | TEMPLATE=$ROOT/cfg/echo-test-template.cfg 76 | 77 | scan_args "$@" 78 | 79 | trap cleanup SIGINT SIGHUP EXIT 80 | 81 | if [[ "${#ARGS[@]}" -eq 1 ]]; then 82 | TEMPLATE="${ARGS[0]}" 83 | if [[ ! -f "$TEMPLATE" ]]; then 84 | echo "Could not find config file: $TEMPLATE" 85 | exit 1 86 | fi 87 | if [ -z "$DOMAIN" ]; then 88 | DOMAIN=$(grep "'domain':" $TEMPLATE | sed -r "s/\s*'domain': '([[:alnum:]\.]+)'.*/\1/g") 89 | fi 90 | fi 91 | 92 | content=$(cat $TEMPLATE) 93 | 94 | if [[ "${#ARGS[@]}" -eq 0 ]]; then 95 | content=$(echo "$content" | sed -r s/USER/$USER/g) 96 | content=$(echo "$content" | sed -r s/PASSWORD/$PASSWORD/g) 97 | content=$(echo "$content" | sed -r "s|URI|$URI|g") 98 | content=$(echo "$content" | sed -r s/DOMAIN/$DOMAIN/g) 99 | content=$(echo "$content" | sed -r s/ROOMS/$ROOMS/g) 100 | fi 101 | 102 | CONFIG=$(mktemp) 103 | echo "$content" > $CONFIG 104 | $ROOT/tools/matrix-bot -c "$CONFIG" 105 | -------------------------------------------------------------------------------- /matrixbot/plugins/feeder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import feedparser 4 | import pytz 5 | import time 6 | from datetime import datetime, timedelta 7 | from matrixbot import utils 8 | from dateutil import parser 9 | 10 | def utcnow(): 11 | now = datetime.utcnow() 12 | return now.replace(tzinfo=pytz.utc) 13 | 14 | class FeederPlugin: 15 | def __init__(self, bot, settings): 16 | self.name = "FeederPlugin" 17 | self.logger = utils.get_logger() 18 | self.bot = bot 19 | self.settings = settings 20 | self.logger.info("FeederPlugin loaded (%(name)s)" % settings) 21 | self.timestamp = {} 22 | for feed in list(self.settings["feeds"].keys()): 23 | self.timestamp[feed] = utcnow() 24 | self.lasttime = time.time() 25 | self.period = self.settings.get('period', 60) 26 | 27 | def pretty_entry(self, entry): 28 | title = entry.get("title", "New post") 29 | author = entry.get("author", "") 30 | if author is not "": 31 | author = " by %s" % author 32 | link = entry.get("link", "") 33 | if link is not "": 34 | link = " (%s)" % link 35 | res = """%s%s%s""" % (title, author, link) 36 | return res 37 | 38 | def dispatch(self, handler): 39 | self.logger.debug("FeederPlugin dispatch") 40 | now = time.time() 41 | if now < self.lasttime + self.period: 42 | return # Feeder is only updated each 'period' time 43 | self.lasttime = now 44 | 45 | res = [] 46 | for feed_name, feed_url in list(self.settings["feeds"].items()): 47 | self.logger.debug("FeederPlugin dispatch: Fetching %s ..." % feed_name) 48 | try: 49 | feed = feedparser.parse(feed_url) 50 | updated = feed.get('feed',{}).get( 51 | 'updated', 52 | utcnow().isoformat() 53 | ) 54 | updated_dt = parser.parse(updated) 55 | if updated_dt > self.timestamp[feed_name]: 56 | actual_updated_dt = self.timestamp[feed_name] 57 | for entry in feed['entries']: 58 | entry_dt = parser.parse(entry["updated"]) 59 | if entry_dt > self.timestamp[feed_name]: 60 | res.append(entry) 61 | actual_updated_dt = max (entry_dt, actual_updated_dt) 62 | self.timestamp[feed_name] = actual_updated_dt 63 | except Exception as e: 64 | self.logger.error("FeederPlugin got error in feed %s: %s" % (feed_name,e)) 65 | 66 | if len(res) == 0: 67 | return 68 | 69 | res = list(map( 70 | self.pretty_entry, 71 | res 72 | )) 73 | message = "\n".join(res) 74 | for room_id in self.settings["rooms"]: 75 | room_id = self.bot.get_real_room_id(room_id) 76 | self.bot.send_notice(room_id, message) 77 | 78 | def command(self, sender, room_id, body, handler): 79 | self.logger.debug("FeederPlugin command") 80 | return 81 | 82 | def help(self, sender, room_id, handler): 83 | self.logger.debug("FeederPlugin help") 84 | return 85 | -------------------------------------------------------------------------------- /matrixbot/plugins/trac.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import xmlrpc.client 4 | from datetime import datetime, timedelta 5 | from matrixbot import utils 6 | 7 | class TracPlugin: 8 | def __init__(self, bot, settings): 9 | self.logger = utils.get_logger() 10 | self.name = "TracPlugin" 11 | self.bot = bot 12 | self.settings = settings 13 | self.logger.info("TracPlugin loaded (%(name)s)" % settings) 14 | self.timestamp = datetime.utcnow() 15 | self.server = xmlrpc.client.ServerProxy( 16 | '%(url_protocol)s://%(url_auth_user)s:%(url_auth_password)s@%(url_domain)s%(url_path)s/login/xmlrpc' % self.settings 17 | ) 18 | 19 | def pretty_ticket(self, ticket): 20 | ticket[3]["ticket_id"] = ticket[0] 21 | url = '%(url_protocol)s://%(url_domain)s%(url_path)s' % self.settings 22 | ticket[3]["ticket_url"] = "%s/ticket/%s" % (url, ticket[0]) 23 | res = """%(summary)s: 24 | * URL: %(ticket_url)s 25 | * [severity: %(severity)s] [owner: %(owner)s] [reporter: %(reporter)s] [status: %(status)s]""" % ticket[3] 26 | return res 27 | 28 | def dispatch(self, handler): 29 | self.logger.debug("TracPlugin dispatch") 30 | server = self.server 31 | 32 | d = self.timestamp 33 | self.timestamp = datetime.utcnow() 34 | res = [] 35 | for t in server.ticket.getRecentChanges(d): 36 | ticket = server.ticket.get(t) 37 | changes = server.ticket.changeLog(t) 38 | if len(changes) == 0 and 'new' in self.settings['status']: # No changes implies New ticket 39 | res.append(ticket) 40 | for c in changes: 41 | if ( 42 | c[0] > d and c[2] == 'status' 43 | and c[4] in self.settings['status'] 44 | ): 45 | res.append(ticket) 46 | 47 | if len(res) == 0: 48 | return 49 | 50 | res = list(map( 51 | self.pretty_ticket, 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("TracPlugin command") 61 | plugin_name = self.settings["name"] 62 | 63 | # TODO: This should be a decorator 64 | if self.bot.only_local_domain and not self.bot.is_local_user_id(sender): 65 | self.logger.warning( 66 | "TracPlugin %s plugin is not allowed for external sender (%s)" % (plugin_name, sender) 67 | ) 68 | return 69 | 70 | sender = sender.replace('@','') 71 | sender = sender.split(':')[0] 72 | command_list = body.split()[1:] 73 | if len(command_list) > 0 and command_list[0] == plugin_name: 74 | if command_list[1] == "create": 75 | summary = ' '.join(command_list[2:]) 76 | self.logger.debug( 77 | "TracPlugin command: %s(%s)" % ( 78 | "create", summary 79 | ) 80 | ) 81 | self.server.ticket.create( 82 | summary, 83 | "", 84 | {"cc": sender}, 85 | True 86 | ) 87 | 88 | def help(self, sender, room_id, handler): 89 | self.logger.debug("TracPlugin help") 90 | if self.bot.is_private_room(room_id, self.bot.get_user_id()): 91 | message = "%(name)s create This is the issue summary\n" % self.settings 92 | else: 93 | message = "%(username)s: %(name)s create This is the issue summary\n" % self.settings 94 | handler(room_id, message) 95 | -------------------------------------------------------------------------------- /tools/matrix-digest: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # -*- coding:utf-8 -*- 4 | # 5 | # Author: Pablo Saavedra 6 | # Maintainer: Pablo Saavedra 7 | # Contact: saavedra.pablo at gmail.com 8 | 9 | import argparse 10 | import traceback 11 | import time 12 | import sys 13 | 14 | from matrixbot import utils 15 | from matrixbot import matrix 16 | 17 | ## vars ######################################################################## 18 | conffile = "./matrixbot.cfg" 19 | days = 1 20 | sendto = "no-reply@example.domain" 21 | 22 | ## command line options parser ################################################# 23 | parser = argparse.ArgumentParser() 24 | parser.add_argument("-c", "--conffile", dest="conffile", default=conffile, 25 | help="Conffile (default: %s)" % conffile) 26 | parser.add_argument("-s", "--sendto", dest="sendto", default=sendto, 27 | help="Send to (default: %s)" % sendto) 28 | parser.add_argument("-d", "--days", type=int, dest="days", default=days, 29 | help="Days (default: %s)" % days) 30 | 31 | parser.add_argument("room", metavar="ROOM", type=str, help="Room") 32 | args = parser.parse_args() 33 | conffile = args.conffile 34 | days = args.days 35 | room = args.room 36 | sendto = args.sendto 37 | 38 | # setting up ################################################################### 39 | settings = utils.get_default_settings() 40 | utils.setup(conffile, settings) 41 | logger = utils.create_logger(settings) 42 | 43 | 44 | import time 45 | import datetime 46 | 47 | MAX_ITERS = 10 48 | 49 | ## main #################################################################### 50 | if __name__ == '__main__': 51 | try: 52 | m = matrix.MatrixBot(settings) 53 | m.join_rooms(silent=True) 54 | token = m.sync_token 55 | 56 | room_id = m.get_real_room_id(room) 57 | 58 | seconds = days * 24 * 3600 59 | now = time.time() 60 | messages = [] 61 | replies = {} 62 | end = False 63 | i = 0 64 | max_iters = MAX_ITERS 65 | while True: 66 | r = m.call_api("get_room_messages", 1, room_id, token, "b", 500) 67 | for c in r["chunk"]: 68 | if now - seconds > c["origin_server_ts"] / 1000: 69 | end = True 70 | break 71 | if c.get("type", "") == "m.room.message": 72 | c_in_reply_to = utils.get_in_reply_to(c) 73 | if c_in_reply_to: 74 | if c_in_reply_to in replies: 75 | replies[c_in_reply_to].append(c) 76 | else: 77 | replies[c_in_reply_to]= [c] 78 | else: 79 | messages.append(c) 80 | if end: 81 | break 82 | i += 1 83 | if i == max_iters: 84 | logger.warning("Max. iters number reached") 85 | break 86 | token = r["end"] 87 | 88 | if not len(messages): 89 | sys.exit(0) 90 | 91 | content = "" 92 | for message in reversed(messages): 93 | content += utils.mail_format_event(message, replies) 94 | 95 | aliases = m.get_room_aliases(room_id) 96 | message = ''' 97 | Thread forwarded from %s, 98 | 99 | - 8< ---------------------------------------------------------------- 100 | 101 | %s 102 | - 8< ---------------------------------------------------------------- 103 | 104 | ''' % (aliases if aliases else room, content) 105 | try: 106 | m.send_mail(message, sendto) 107 | except matrix.MatrixBotError as e: 108 | traceback.print_exc(e) 109 | logger.error("%s" % e) 110 | except Exception as e: 111 | traceback.print_exc() 112 | logger.error("error: %s" % e) 113 | except Exception as e: 114 | traceback.print_exc() 115 | logger.error("Unexpected error: %s" % e) 116 | 117 | -------------------------------------------------------------------------------- /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["mail"] = { 14 | "host": "127.0.0.1", 15 | "port": 25, 16 | "ssl": False, 17 | "subject": "Matrix bot", 18 | "from": "bot@domain.com", 19 | "username": "username", 20 | "password": "password", 21 | "to_policy": "deny", # or allow 22 | "to_policy_filter": "all" # None or ["domain.com", "mailbox@second-domain.com"] 23 | } 24 | settings["memcached"] = { 25 | "ip": "127.0.0.1", 26 | "port": 11211, 27 | "timeout": 300, 28 | } 29 | settings["ldap"] = { 30 | "server": "ldap://ldap.local", 31 | "base": "ou=People,dc=example,dc=com", 32 | "groups": [], 33 | "groups_id": "cn", 34 | "groups_filter": "(objectClass=posixGroup)", 35 | "groups_base": "ou=Group,dc=example,dc=com", 36 | "users_aliases": { 37 | "user1":"username1", 38 | }, 39 | } 40 | settings["aliases"] = { 41 | "simple_invite":"invite +group1 +group2", 42 | "simple_kick":"invite +group1 +group2", 43 | } 44 | settings["subscriptions"] = { 45 | "#room_alias1":"@user1 @user2 but @user3", 46 | "#room_alias2":"+group1 +group2 but @user1", 47 | } 48 | settings["revokations"] = { 49 | "#room_alias1":"@user1 @user2 but @user3", 50 | "#room_alias2":"+group1 +group2 but @user1", 51 | } 52 | settings["allowed-join"] = { 53 | "default": "+group1 +group2 but @user1", 54 | "#room_alias1": "+group1 +group2", 55 | } 56 | 57 | 58 | # settings["plugins"] = {} 59 | 60 | # plugin_trac={} 61 | # plugin_trac["module"] = "matrixbot.plugins.trac" 62 | # plugin_trac["class"] = "TracPlugin" 63 | # plugin_trac["settings"] = { 64 | # "username": "username", 65 | # "name": "trac", 66 | # "rooms": ["!room_id"], 67 | # "url_protocol": "https", 68 | # "url_domain": "trac", 69 | # "url_path": "/tracker", 70 | # "url_auth_user": "user", 71 | # "url_auth_password": "password", 72 | # "status": ['new', 'reopened', 'closed'], 73 | # } 74 | # 75 | # settings["plugins"]["plugin_trac"] = plugin_trac 76 | 77 | # plugin_broadcast={} 78 | # plugin_broadcast["module"] = "matrixbot.plugins.broadcast" 79 | # plugin_broadcast["class"] = "BroadcastPlugin" 80 | # plugin_broadcast["settings"] = { 81 | # "username": "username", 82 | # "name": "broadcast", 83 | # "users": ["@user1"], 84 | # "rooms": ["!room_id", 85 | # "#room_alias"], 86 | # } 87 | # 88 | # settings["plugins"]["plugin_broadcast"] = plugin_broadcast 89 | 90 | # plugin_feeder={} 91 | # plugin_feeder["module"] = "matrixbot.plugins.feeder" 92 | # plugin_feeder["class"] = "FeederPlugin" 93 | # plugin_feeder["settings"] = { 94 | # "period": 60, 95 | # "username": "username", 96 | # "name": "feed", 97 | # "rooms": ["!room_id"], 98 | # "feeds": { 99 | # "matrix-bot": "https://github.com/psaavedra/matrix-bot/commits/master.atom", 100 | # }, 101 | # } 102 | # 103 | # settings["plugins"]["plugin_feeder"] = plugin_feeder 104 | 105 | # plugin_wk={} 106 | # plugin_wk["module"] = "matrixbot.plugins.wkbotsfeeder" 107 | # plugin_wk["class"] = "WKBotsFeederPlugin" 108 | # plugin_wk["settings"] = { 109 | # "username": "bot", 110 | # "period": 10, 111 | # "name": "wk", 112 | # "rooms": ["!room_id"], 113 | # "builders": { 114 | # "WPE Linux 64-bit Release (Build)": { 115 | # "last_buildjob_url_squema": "https://build.webkit.org/builders/%(builder_name)s/builds/%(last_buildjob)s", 116 | # "builds_url_squema": "https://build.webkit.org/json/builders/%(builder_name)s/builds?select=-2&as_text=1", 117 | # "builder_name": "WPE Linux 64-bit Release (Build)", 118 | # "only_failures": False, 119 | # "target_step": { 120 | # "name": "compile-webkit", 121 | # "text": "compiled" 122 | # } 123 | # }, 124 | # } 125 | # settings["plugins"]["plugin_feeder"] = plugin_feeder 126 | -------------------------------------------------------------------------------- /matrixbot/ldap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # -*- coding:utf-8 -*- 4 | # 5 | # Author: Pablo Saavedra 6 | # Maintainer: Pablo Saavedra 7 | # Contact: saavedra.pablo at gmail.com 8 | 9 | 10 | 11 | import ldap as LDAP 12 | 13 | from . import utils 14 | 15 | 16 | def get_custom_ldap_group_members(ldap_settings, group_name): 17 | logger = utils.get_logger() 18 | ldap_server = ldap_settings["server"] 19 | ldap_base = ldap_settings["base"] 20 | get_uid = lambda x: x[1]["uid"][0].decode("utf-8") 21 | members = [] 22 | try: 23 | conn = LDAP.initialize(ldap_server) 24 | g_ldap_filter = ldap_settings[group_name] 25 | logger.debug("Searching members for %s: %s" % (group_name, 26 | g_ldap_filter)) 27 | items = conn.search_s(ldap_base, LDAP.SCOPE_SUBTREE, 28 | attrlist=['uid'], 29 | filterstr=g_ldap_filter) 30 | members = list(map(get_uid, items)) 31 | except Exception as e: 32 | logger.error("Error getting custom group %s from LDAP: %s" % (group_name, e)) 33 | return members 34 | 35 | 36 | def get_ldap_group_members(ldap_settings, group_name): 37 | # base:dc=example,dc=com 38 | # filter:(&(objectClass=posixGroup)(cn={group_name})) 39 | logger = utils.get_logger() 40 | ldap_server = ldap_settings["server"] 41 | ldap_base = ldap_settings["groups_base"] 42 | ldap_filter = "(&%s(%s={group_name}))" % (ldap_settings["groups_filter"], ldap_settings["groups_id"]) 43 | get_uid = lambda x: x.decode("utf-8").split(",")[0].split("=")[1] 44 | try: 45 | ad_filter = ldap_filter.replace('{group_name}', group_name) 46 | conn = LDAP.initialize(ldap_server) 47 | logger.debug("Searching members for %s: %s - %s - %s" % (group_name, 48 | ldap_server, 49 | ldap_base, 50 | ad_filter)) 51 | res = conn.search_s(ldap_base, LDAP.SCOPE_SUBTREE, ad_filter) 52 | except Exception as e: 53 | logger.error("Error getting group from LDAP: %s" % e) 54 | 55 | return list(map(get_uid, res[0][1]['uniqueMember'])) 56 | 57 | 58 | def get_ldap_groups(ldap_settings): 59 | '''Returns the a list of found LDAP groups filtered with the groups list in 60 | the settings 61 | ''' 62 | # filter:(objectClass=posixGroup) 63 | # base:ou=Group,dc=example,dc=com 64 | logger = utils.get_logger() 65 | ldap_server = ldap_settings["server"] 66 | ldap_base = ldap_settings["groups_base"] 67 | ldap_filter = ldap_settings["groups_filter"] 68 | ldap_groups = ldap_settings["groups"] 69 | get_uid = lambda x: x[1]["cn"][0].decode("utf-8") 70 | try: 71 | conn = LDAP.initialize(ldap_server) 72 | logger.debug("Searching groups: %s - %s - %s" % (ldap_server, 73 | ldap_base, 74 | ldap_filter)) 75 | res = conn.search_s(ldap_base, LDAP.SCOPE_SUBTREE, ldap_filter) 76 | return list(filter((lambda x: x in ldap_groups), list(map(get_uid, res)))) 77 | except Exception as e: 78 | logger.error("Error getting groups from LDAP: %s (%s)" % (e, ldap_server)) 79 | return [] 80 | 81 | 82 | def get_ldap_groups_members(ldap_settings): 83 | def map_aliases(x): 84 | return ldap_settings.get('users_aliases', {}).get(x, x) 85 | 86 | ldap_groups = ldap_settings["groups"] 87 | groups = get_ldap_groups(ldap_settings) 88 | res = {} 89 | for g in groups: 90 | res[g] = list(map(map_aliases, get_ldap_group_members(ldap_settings, g))) 91 | 92 | # pending groups to get members. filters for those groups are explicitelly 93 | # defined in the settings 94 | custom_groups = list(filter((lambda x: x not in groups), ldap_groups)) 95 | for g in custom_groups: 96 | res[g] = list(map(map_aliases, get_custom_ldap_group_members(ldap_settings, g))) 97 | return res 98 | 99 | 100 | def get_groups(ldap_settings): 101 | return ldap_settings["groups"] 102 | -------------------------------------------------------------------------------- /matrixbot/plugins/wktestbotsfeeder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | import os 5 | import pytz 6 | import re 7 | import requests 8 | import sys 9 | import urllib.request, urllib.parse, urllib.error 10 | import time 11 | 12 | if os.path.dirname(__file__) == "matrixbot/plugins": 13 | sys.path.append(os.path.abspath(".")) 14 | 15 | from matrixbot import utils 16 | 17 | pp, set_property = utils.pp, utils.set_property 18 | 19 | class WKTestBotsFeederPlugin: 20 | def __init__(self, bot, settings): 21 | self.name = "WKTestBotsFeederPlugin" 22 | self.logger = utils.get_logger() 23 | self.bot = bot 24 | self.settings = settings 25 | self.logger.info("WKTestBotsFeederPlugin loaded (%(name)s)" % settings) 26 | for builder_name, builder in list(self.settings["builders"].items()): 27 | if 'builder_name' not in builder: 28 | builder['builder_name'] = builder_name 29 | builder['last_buildjob'] = -1 30 | set_property(self.settings, builder, "last_buildjob_url_schema") 31 | set_property(self.settings, builder, "builds_url_schema") 32 | set_property(self.settings, builder, "only_failures", default=True) 33 | set_property(self.settings, builder, "notify_recoveries", default=True) 34 | self.logger.info("WKTestBotsFeederPlugin loaded (%(name)s) builder: " % settings + json.dumps(builder, indent = 4)) 35 | self.lasttime = time.time() 36 | self.period = self.settings.get('period', 60) 37 | 38 | def pretty_entry(self, builder, summary): 39 | url = self.last_build_url(builder) 40 | 41 | res = "%(builder_name)s " % builder 42 | res += "(" % url 43 | res += "%(last_buildjob)s ): " % builder 44 | 45 | if builder['recovery']: 46 | res += "

%s (%s)

" % (pp("Recovery", color="green", strong=True), 47 | pp(summary, color="green")) 48 | elif builder['failed']: 49 | res += "

%s (%s)

" % (pp("Exiting early", color="red", strong=True), 50 | pp(summary, color="red")) 51 | else: 52 | res += "

%s (%s)

" % (pp("Success", color="green", strong=True), 53 | pp(summary, color="green")) 54 | return res 55 | 56 | def last_build_url(self, builder): 57 | builderid = int(builder['builderid']) 58 | build_number = int(builder['last_buildjob']) 59 | return builder['last_buildjob_url_schema'] % (builderid, build_number) 60 | 61 | def send(self, message): 62 | for room_id in self.settings["rooms"]: 63 | room_id = self.bot.get_real_room_id(room_id) 64 | self.bot.send_html(room_id, message, msgtype="m.notice") 65 | 66 | def has_failed(self, build): 67 | return re.search('exiting early', build['state_string'], re.IGNORECASE) != None 68 | 69 | def was_exception(self, build): 70 | return re.search('exception', build['state_string'], re.IGNORECASE) != None 71 | 72 | def summary(self, build): 73 | return build['state_string'] 74 | 75 | def dispatch(self, handler=None): 76 | self.logger.debug("WKTestBotsFeederPlugin dispatch") 77 | now = time.time() 78 | if now < self.lasttime + self.period: 79 | return # Feeder is only updated each 'period' time 80 | self.lasttime = now 81 | 82 | res = [] 83 | for builder_name, builder in list(self.settings["builders"].items()): 84 | self.logger.debug("WKTestBotsFeederPlugin dispatch: Fetching %s ..." % builder_name) 85 | try: 86 | build = self.get_last_build(builder) 87 | if builder['last_buildjob'] >= int(build['number']): 88 | continue 89 | 90 | failed = self.has_failed(build) 91 | builder.update({ 92 | 'failed': failed, 93 | 'last_buildjob': int(build['number']), 94 | 'recovery': 'failed' in builder and builder['failed'] and not failed and not self.was_exception(build), 95 | }) 96 | 97 | if self.should_send_message(builder, failed): 98 | message = self.pretty_entry(builder, self.summary(build)) 99 | self.send(message) 100 | except Exception as e: 101 | self.logger.error("WKTestBotsFeederPlugin got error in builder %s: %s" % (builder_name,e)) 102 | 103 | def should_send_message(self, builder, failed): 104 | return failed or (not builder['only_failures']) or (builder['notify_recoveries'] and builder['recovery']) 105 | 106 | def get_last_build(self, builder): 107 | url = builder['builds_url_schema'] % builder['builderid'] 108 | ret = requests.get(url).json() 109 | return ret['builds'][0] 110 | 111 | def command(self, sender, room_id, body, handler): 112 | self.logger.debug("WKTestBotsFeederPlugin command") 113 | return 114 | 115 | def help(self, sender, room_id, handler): 116 | self.logger.debug("WKTestBotsFeederPlugin help") 117 | return 118 | 119 | 120 | def selftest(): 121 | print("selftest: " + os.path.basename(__file__)) 122 | settings = { 123 | "name": "wk", 124 | "last_buildjob_url_schema": "https://build.webkit.org/#/builders/%d/builds/%d", 125 | "builds_url_schema": "https://build.webkit.org/api/v2/builders/%d/builds?complete=true&order=-number&limit=1", 126 | "only_failures": False, 127 | "rooms": ["0"], 128 | "builders": { 129 | "GTK-Linux-64-bit-Debug-Tests": { 130 | "builderid": 63, 131 | }, 132 | }, 133 | } 134 | plugin = WKTestBotsFeederPlugin(utils.MockBot(), settings) 135 | 136 | test_dispatch(plugin) 137 | test_can_fetch_last_build(plugin) 138 | 139 | def test_dispatch(plugin): 140 | print("test_dispatch: ") 141 | import logging 142 | logging.basicConfig(level = logging.DEBUG) 143 | plugin.lasttime = 0 144 | plugin.period = 0 145 | plugin.dispatch() 146 | print("") 147 | print("Ok") 148 | 149 | def test_can_fetch_last_build(plugin): 150 | puts = sys.stdout.write 151 | puts("test_can_fetch_last_build: ") 152 | builder = plugin.settings['builders']["GTK-Linux-64-bit-Debug-Tests"] 153 | build = plugin.get_last_build(builder) 154 | assert(build) 155 | print("Ok") 156 | 157 | if __name__ == '__main__': 158 | selftest() 159 | -------------------------------------------------------------------------------- /matrixbot/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # -*- coding:utf-8 -*- 4 | # 5 | # Author: Pablo Saavedra 6 | # Maintainer: Pablo Saavedra 7 | # Contact: saavedra.pablo at gmail.com 8 | 9 | import sys 10 | import logging 11 | import copy 12 | import memcache 13 | import imp 14 | 15 | from datetime import datetime, timedelta 16 | from dateutil import parser 17 | 18 | puts = sys.stdout.write 19 | 20 | def get_default_settings(): 21 | settings = {} 22 | settings["DEFAULT"] = { 23 | "loglevel": 10, 24 | "logfile": "/dev/stdout", 25 | "period": 30, 26 | } 27 | settings["mail"] = { 28 | "host": "127.0.0.1", 29 | "port": 25, 30 | "ssl": False, 31 | "subject": "Matrix bot", 32 | "from": "bot@domain.com", 33 | "username": "username", 34 | "password": "password", 35 | "to_policy": "deny", 36 | "to_policy_filter": "all", 37 | } 38 | settings["memcached"] = { 39 | "ip": "127.0.0.1", 40 | "port": 11211, 41 | "timeout": 300, 42 | } 43 | settings["matrix"] = { 44 | "uri": "http://localhost:8000", 45 | "username": "username", 46 | "password": "password", 47 | "domain": "matrix.org", 48 | "rooms": [], 49 | "only_local_domain": False, 50 | "super_users": [], 51 | } 52 | settings["ldap"] = { 53 | "server": "ldap://ldap.local", 54 | "base": "ou=People,dc=example,dc=com", 55 | "groups": [], 56 | "groups_id": "cn", 57 | "groups_filter": "(objectClass=posixGroup)", 58 | "groups_base": "ou=Group,dc=example,dc=com", 59 | "users_aliases": {}, 60 | } 61 | settings["aliases"] = { 62 | } 63 | settings["subscriptions"] = { 64 | } 65 | settings["revokations"] = { 66 | } 67 | settings["allowed-join"] = { 68 | "default": "" 69 | } 70 | settings["plugins"] = {} 71 | settings["commands"] = { 72 | "enable": True, 73 | "list-rooms": { 74 | "enable": False, 75 | "visible_subset": [], 76 | }, 77 | } 78 | return settings 79 | 80 | 81 | def debug_conffile(settings, logger): 82 | for s in list(settings.keys()): 83 | for k in list(settings[s].keys()): 84 | key = "%s.%s" % (s, k) 85 | if k in ["username", "password"]: 86 | value = "XXXXXXXX" 87 | else: 88 | value = settings[s][k] 89 | logger.debug("Configuration setting - %s: %s" % (key, value)) 90 | 91 | 92 | def setup(conffile, settings): 93 | try: 94 | imp.reload(sys) 95 | # Forcing UTF-8 in the enviroment: 96 | sys.setdefaultencoding('utf-8') 97 | # http://stackoverflow.com/questions/3828723/why-we-need-sys-setdefaultencodingutf-8-in-a-py-scrip 98 | except Exception: 99 | pass 100 | exec(compile(open(conffile).read(), conffile, 'exec')) 101 | 102 | 103 | def create_cache(settings): 104 | cache = memcache.Client(['%(ip)s:%(port)s' % settings["memcached"]], debug=0) 105 | return cache 106 | 107 | def create_logger(settings): 108 | logfile = settings["DEFAULT"]["logfile"] 109 | if (logfile == "/dev/stdout"): 110 | hdlr = logging.StreamHandler(sys.stdout) 111 | elif (logfile == "/dev/stderr"): 112 | hdlr = logging.StreamHandler(sys.stderr) 113 | else: 114 | hdlr = logging.FileHandler(logfile) 115 | hdlr.setFormatter(logging.Formatter('%(levelname)s %(asctime)s %(message)s')) 116 | logger = logging.getLogger('matrixbot') 117 | logger.addHandler(hdlr) 118 | logger.setLevel(settings["DEFAULT"]["loglevel"]) 119 | logger.debug("Default encoding: %s" % sys.getdefaultencoding()) 120 | debug_conffile(settings, logger) 121 | return logger 122 | 123 | 124 | def get_logger(): 125 | return logging.getLogger('matrixbot') 126 | 127 | 128 | def get_command_alias(message, settings): 129 | prefix = message.strip().split()[0] 130 | command = " ".join(message.strip().split()[1:]) 131 | if command in list(settings["aliases"].keys()): 132 | return prefix + " " + settings["aliases"][command] 133 | return message 134 | 135 | 136 | def get_aliases(settings): 137 | res = copy.copy(settings["aliases"]) 138 | return res 139 | 140 | 141 | def set_property(settings, builder, setting, default=None): 142 | if setting in builder: 143 | return 144 | if setting in settings: 145 | builder[setting] = settings[setting] 146 | else: 147 | builder[setting] = default 148 | 149 | 150 | def utcnow(): 151 | now = datetime.utcnow() 152 | return now.replace(tzinfo=pytz.utc) 153 | 154 | 155 | def pp(text, **kwargs): 156 | ret="{content}" 157 | for key in kwargs: 158 | if key == "color": 159 | ret = ret.format(content="{{content}}".format(color=kwargs['color'])) 160 | else: 161 | ret = ret.format(content="<{tag}>{{content}}".format(tag=key)) 162 | return ret.format(content=text) 163 | 164 | 165 | def list_to_str(l): 166 | return " ".join(l) if len(l) > 0 else "no one" 167 | 168 | 169 | def mail_format_event(event, replies=[], hide_matrix_domain=True, prefix=""): 170 | f = "[%s]%s%s: %s\n" 171 | d = datetime.utcfromtimestamp(event['origin_server_ts'] / 1000).strftime('%Y-%m-%d %H:%M:%S UTC') 172 | 173 | if event['event_id'] in replies: 174 | event['replies'] = replies[event['event_id']] 175 | else: 176 | event['replies'] = [] 177 | 178 | sender = event['sender'].split(':')[0] if hide_matrix_domain else event['sender'] 179 | if is_reply(event): 180 | body = ' '.join(event['content']['body'].split('\n\n')[1:]) 181 | else: 182 | body = event['content']['body'] 183 | sender = "[%s]" % sender 184 | content = f % (d, prefix, sender, body) 185 | 186 | for r in reversed(event['replies']): 187 | content += mail_format_event(r, replies, hide_matrix_domain, 188 | ''.join([' ' for _ in prefix]) + ' ↪️ ') 189 | return content 190 | 191 | 192 | def get_in_reply_to(event): 193 | return event['content'].get('m.relates_to', {}).get('m.in_reply_to', {}).get('event_id', None) 194 | 195 | 196 | def is_reply(event): 197 | in_reply_to = get_in_reply_to(event) 198 | body = event["content"]["body"] 199 | # If the body looks like a reply, remove the first 2 tokens 200 | # > <@user:domain.com> body example 201 | return in_reply_to and body.startswith('> <@') 202 | 203 | 204 | class MockBot: 205 | def __init__(self): 206 | pass 207 | 208 | def get_real_room_id(self, room_id): 209 | return room_id or 0 210 | 211 | def send_html(self, room_id, message, **kwargs): 212 | puts("Room #{}: {}".format(room_id, message)) 213 | -------------------------------------------------------------------------------- /matrixbot/plugins/wkbotsfeeder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | import logging 5 | import os 6 | import pytz 7 | import requests 8 | import sys 9 | import urllib.request, urllib.parse, urllib.error 10 | import time 11 | import datetime 12 | 13 | if os.path.dirname(__file__) == "matrixbot/plugins": 14 | sys.path.append(os.path.abspath(".")) 15 | 16 | from matrixbot import utils 17 | 18 | pp, puts, set_property = utils.pp, utils.puts, utils.set_property 19 | 20 | SIX_HOURS = 6 * (60 * 60) 21 | 22 | class WKBotsFeederPlugin: 23 | def __init__(self, bot, settings): 24 | self.name = "WKBotsFeederPlugin" 25 | self.logger = utils.get_logger() 26 | self.bot = bot 27 | self.load(settings) 28 | 29 | def load(self, settings): 30 | self.settings = settings 31 | self.logger.info("WKBotsFeederPlugin loaded (%(name)s)" % settings) 32 | for builder_name, builder in list(self.settings["builders"].items()): 33 | if 'builder_name' not in builder: 34 | builder['builder_name'] = builder_name 35 | builder['last_buildjob'] = -1 36 | set_property(self.settings, builder, "last_buildjob_url_schema") 37 | set_property(self.settings, builder, "builds_url_schema") 38 | set_property(self.settings, builder, "only_failures", default=True) 39 | set_property(self.settings, builder, "notify_recoveries", default=True) 40 | self.logger.info("WKBotsFeederPlugin loaded (%(name)s) builder: " % settings + json.dumps(builder, indent = 4)) 41 | self.lasttime = time.time() 42 | self.period = self.settings.get('period', 60) 43 | 44 | def pretty_message(self, builder, msg): 45 | url = self.last_build_url(builder) 46 | 47 | res = "%(builder_name)s " % builder 48 | res += "(" % url 49 | res += "%(last_buildjob)s ): " % builder 50 | res += msg 51 | 52 | return res 53 | 54 | def last_build_url(self, builder): 55 | builderid = int(builder['builderid']) 56 | build_number = int(builder['last_buildjob']) 57 | return builder['last_buildjob_url_schema'] % (builderid, build_number) 58 | 59 | def send(self, message): 60 | for room_id in self.settings["rooms"]: 61 | room_id = self.bot.get_real_room_id(room_id) 62 | self.bot.send_html(room_id, message, msgtype="m.notice") 63 | 64 | def should_send_message(self, builder, failed): 65 | if "mute" in builder: 66 | return False 67 | return failed or (not builder['only_failures']) or (builder['notify_recoveries'] and builder['recovery']) 68 | 69 | def failed(self, build, value = "build successful"): 70 | return not self.succeeded(build, value) 71 | 72 | def succeeded(self, build, value = "build successful"): 73 | return 'state_string' in build and build['state_string'] == value 74 | 75 | def get_step(self, builder, build, stepname): 76 | builderid = int(builder['builderid']) 77 | buildNumber = int(build['number']) 78 | url = "https://build.webkit.org/api/v2/builders/%d/builds/%d/steps?name=%s" % (builderid, buildNumber, stepname) 79 | ret = requests.get(url).json() 80 | return ret['steps'][0] 81 | 82 | def get_last_build(self, builder): 83 | url = builder['builds_url_schema'] % builder['builderid'] 84 | ret = requests.get(url).json() 85 | return ret['builds'][0] 86 | 87 | def dispatch(self, handler=None): 88 | self.logger.debug("WKBotsFeederPlugin dispatch") 89 | now = time.time() 90 | if now < self.lasttime + self.period: 91 | return # Feeder is only updated each 'period' time 92 | self.lasttime = now 93 | 94 | for builder_name, builder in list(self.settings["builders"].items()): 95 | self.logger.debug("WKBotsFeederPlugin dispatch: Fetching %s ..." % builder_name) 96 | try: 97 | build = self.get_last_build(builder) 98 | 99 | if 'stopped' not in builder: 100 | builder['stopped'] = False 101 | 102 | if not builder['stopped'] and bool(build["complete"]) and int(build["complete_at"]) + SIX_HOURS < now: 103 | date = datetime.datetime.fromtimestamp(int(build["complete_at"])) 104 | self.pretty_message(builder, "Last successful build completed on '%s'. Bot may be stopped." % date.strftime("%Y-%m-%d %H:%M:%S")) 105 | builder['stopped'] = True 106 | continue 107 | 108 | if builder['stopped'] and bool(build["complete"]) and int(build["complete_at"]) + SIX_HOURS >= now: 109 | builder['stopped'] = False 110 | 111 | if builder['last_buildjob'] >= build['number']: 112 | continue 113 | if 'target_step' in builder: 114 | target_step = builder['target_step'] 115 | step = self.get_step(builder, build, target_step['name']) 116 | failed = self.failed(step, target_step['text']) 117 | else: 118 | failed = self.failed(build) 119 | 120 | builder.update({ 121 | 'failed': failed, 122 | 'last_buildjob': int(build['number']), 123 | 'recovery': 'failed' in builder and builder['failed'] and not failed 124 | }) 125 | 126 | if self.should_send_message(builder, failed): 127 | self.logger.debug("WKBotsFeederPlugin: Should send message") 128 | if builder['recovery']: 129 | self.send(self.pretty_message(builder, pp("recovery", color="green"))) 130 | elif builder['failed']: 131 | self.send(self.pretty_message(builder, pp("failed", color="red"))) 132 | else: 133 | self.send(self.pretty_message(builder, pp("success", color="green"))) 134 | except Exception as e: 135 | self.logger.error("WKBotsFeederPlugin got error in builder %s: %s" % (builder_name,e)) 136 | 137 | def command(self, sender, room_id, body, handler = None): 138 | self.logger.debug("WKBotsFeederPlugin command: %s" % body) 139 | if len(body) == 0: 140 | return 141 | command = body.split() 142 | if command[0] != self.settings["name"]: 143 | return 144 | if len(command) > 1 and command[1].lower() == "mute": 145 | self.command_mute(sender, room_id, command[2:], handler) 146 | else: 147 | self.bot.send_html(room_id, "

Unknown command: %s

" % body) 148 | 149 | def command_mute(self, sender, room_id, command, handler): 150 | # Command: mute [ON|OFF]. 151 | if len(command) == 0: 152 | self.help(sender, room_id, handler) 153 | return 154 | builders = self.settings["builders"] 155 | builderName = command[0] 156 | if builderName not in builders.keys(): 157 | self.help(sender, room_id, handler) 158 | return 159 | value = (command[1] if command[1:] else "on").lower() 160 | if value not in ["on", "off"]: 161 | self.help(sender, room_id, handler) 162 | return 163 | if value == "on": 164 | builders[builderName]["mute"] = True 165 | else: 166 | builders[builderName].pop("mute") 167 | self.bot.send_html(room_id, "

Builder '%s' was set to mute %s

" % (builderName, value)) 168 | 169 | def help(self, sender, room_id, handler): 170 | if not handler: 171 | return 172 | self.logger.debug("WKBotsFeederPlugin help") 173 | message = "mute [ON|OFF]" 174 | handler(room_id, message) 175 | 176 | def selftest(): 177 | print("selftest: " + os.path.basename(__file__)) 178 | 179 | def webkitBuilderSettings(): 180 | return { 181 | "name": "WKBotsFeederPlugin", 182 | "last_buildjob_url_schema": "https://build.webkit.org/#/builders/%d/builds/%d", 183 | "builds_url_schema": "https://build.webkit.org/api/v2/builders/%d/builds?complete=true&order=-number&limit=1", 184 | "only_failures": False, 185 | "rooms": ["0"], 186 | "builders": { 187 | "GTK-Linux-64-bit-Release-Ubuntu-LTS-Build": { 188 | "builderid": 68, 189 | }, 190 | }, 191 | } 192 | def jsCoreBuilderSettings(): 193 | ret = webkitBuilderSettings() 194 | ret["builders"] = { 195 | "JSCOnly-Linux-ARMv7-Thumb2-Release": { 196 | "builderid": 24, 197 | "target_step": { 198 | "name": "compile-jsc", 199 | "text": "compiled" 200 | } 201 | } 202 | } 203 | return ret 204 | 205 | plugin = WKBotsFeederPlugin(utils.MockBot(), webkitBuilderSettings()) 206 | 207 | test_dispatch(plugin) 208 | test_can_fetch_last_build(plugin) 209 | 210 | plugin.load(jsCoreBuilderSettings()) 211 | test_dispatch(plugin) 212 | 213 | plugin.load(webkitBuilderSettings()) 214 | test_mute_command(plugin) 215 | 216 | def test_dispatch(plugin): 217 | print("test_dispatch: ") 218 | logging.basicConfig(level = logging.DEBUG) 219 | plugin.lasttime = 0 220 | plugin.period = 0 221 | plugin.dispatch() 222 | print("") 223 | print("Ok") 224 | print("--") 225 | 226 | def test_can_fetch_last_build(plugin): 227 | puts("test_can_fetch_last_build: ") 228 | builder = plugin.settings['builders']["GTK-Linux-64-bit-Release-Ubuntu-LTS-Build"] 229 | build = plugin.get_last_build(builder) 230 | assert(build) 231 | print("") 232 | print("Ok") 233 | print("--") 234 | 235 | def test_mute_command(plugin): 236 | print("test_mute_command: ") 237 | logging.basicConfig(level = logging.DEBUG) 238 | 239 | sender = "user" 240 | room_id = plugin.settings["rooms"][0] 241 | 242 | # mute . 243 | body = "WKBotsFeederPlugin mute GTK-Linux-64-bit-Release-Ubuntu-LTS-Build" 244 | plugin.command(sender, room_id, body) 245 | assert(plugin.settings['builders']["GTK-Linux-64-bit-Release-Ubuntu-LTS-Build"]["mute"]) 246 | print("") 247 | 248 | # mute off. 249 | body = "WKBotsFeederPlugin mute GTK-Linux-64-bit-Release-Ubuntu-LTS-Build off" 250 | plugin.command(sender, room_id, body) 251 | assert("mute" not in plugin.settings['builders']["GTK-Linux-64-bit-Release-Ubuntu-LTS-Build"]) 252 | print("") 253 | 254 | # mute . 255 | body = "WKBotsFeederPlugin mute GTK-Linux-64-bit-Release-Ubuntu-LTS-Build on" 256 | plugin.command(sender, room_id, body) 257 | assert(plugin.settings['builders']["GTK-Linux-64-bit-Release-Ubuntu-LTS-Build"]["mute"]) 258 | print("") 259 | 260 | print("Ok") 261 | print("--") 262 | 263 | if __name__ == '__main__': 264 | selftest() 265 | -------------------------------------------------------------------------------- /matrixbot/matrix.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # -*- coding:utf-8 -*- 4 | # 5 | # Author: Pablo Saavedra 6 | # Maintainer: Pablo Saavedra 7 | # Contact: saavedra.pablo at gmail.com 8 | 9 | from matrix_client.api import MatrixRequestError 10 | from matrix_client.client import MatrixClient 11 | from matrix_client.room import Room 12 | 13 | import asyncio 14 | # import pprint 15 | import time 16 | import traceback 17 | import re 18 | 19 | import email 20 | import smtplib 21 | import ssl 22 | from email.mime.text import MIMEText 23 | 24 | from . import utils 25 | from . import ldap as bot_ldap 26 | 27 | EXTRA_DEBUG = 5 28 | 29 | class MatrixBotError(Exception): 30 | pass 31 | 32 | class MatrixBot(): 33 | def __init__(self, settings): 34 | self.sync_token = None 35 | 36 | self.logger = utils.get_logger() 37 | self.cache = utils.create_cache(settings) 38 | self.cache_timeout = int(settings["memcached"]["timeout"]) 39 | 40 | self.settings = settings 41 | self.period = settings["DEFAULT"]["period"] 42 | 43 | matrix = settings.get("matrix", {}) 44 | self.uri = matrix["uri"] 45 | self.username = matrix["username"].lower() 46 | self.password = matrix["password"] 47 | self.room_ids = matrix["rooms"] 48 | self.domain = matrix["domain"] 49 | self.only_local_domain = matrix["only_local_domain"] 50 | self.super_users = matrix.get("super_users", []) 51 | 52 | self.commands_enable = settings\ 53 | .get("commands", {})\ 54 | .get("enable", True) 55 | 56 | self.subscriptions_room_ids = settings.get("subscriptions", {}).keys() 57 | self.revokations_rooms_ids = settings.get("revokations", {}).keys() 58 | self.allowed_join_rooms_ids = [x for x in list(settings["allowed-join"].keys()) if x != 'default'] 59 | self.default_allowed_join_rooms = settings\ 60 | .get("allowed-join", {})\ 61 | .get("default", "") 62 | 63 | self.enable_list_rooms_commands = settings\ 64 | .get("commands", {})\ 65 | .get("list-rooms", {})\ 66 | .get("enable", False) 67 | self.visible_subset_list_rooms_commands = settings\ 68 | .get("commands", {})\ 69 | .get("list-rooms", {})\ 70 | .get("visible_subset", []) 71 | 72 | self.client = MatrixClient(self.uri) 73 | self.token = self.client.login_with_password(username=self.username, 74 | password=self.password) 75 | 76 | self.rooms = [] 77 | self.room_aliases = {} 78 | self.plugins = [] 79 | for plugin in list(settings['plugins'].values()): 80 | mod = __import__(plugin['module'], fromlist=[plugin['class']]) 81 | klass = getattr(mod, plugin['class']) 82 | self.plugins.append(klass(self, plugin['settings'])) 83 | 84 | def _get_selected_users(self, groups_users_list): 85 | def _add_or_remove_user(users, username, append): 86 | username = self.normalize_user_id(username) 87 | if append and username not in users["in"]: 88 | users["in"].append(username) 89 | if not append and username not in users["out"]: 90 | users["out"].append(username) 91 | 92 | ldap_settings = self.settings["ldap"] 93 | append = True 94 | users = { 95 | "in": [], 96 | "out": [] 97 | } 98 | for item in groups_users_list: 99 | if item == ("but"): 100 | append = False 101 | elif item.startswith("+"): 102 | group_name = item[1:] 103 | groups_members = bot_ldap.get_ldap_groups_members(ldap_settings) 104 | if group_name in list(groups_members.keys()): 105 | list([_add_or_remove_user(users, x, append) for x in groups_members[group_name]]) 106 | else: 107 | _add_or_remove_user(users, item, append) 108 | 109 | selected_users = [x for x in users["in"] if x not in users["out"]] 110 | return selected_users 111 | 112 | def normalize_user_id(self, user_id): 113 | if not user_id.startswith("@"): 114 | user_id = "@" + user_id 115 | self.logger.debug("Adding missing '@' to the username: %s" % user_id) 116 | if user_id.count(":") == 0: 117 | user_id = "%s:%s" % (user_id, self.domain) 118 | return user_id 119 | 120 | def get_user_id(self, username=None, normalized=True): 121 | if not username: 122 | username = self.username 123 | normalized_username = self.normalize_user_id(username) 124 | if normalized: 125 | return normalized_username 126 | else: 127 | return normalized_username[1:].split(':')[0] 128 | 129 | def is_local_user_id(self, username): 130 | normalized_username = self.get_user_id(username, normalized=True) 131 | if normalized_username.split(':')[1] == self.domain: 132 | return True 133 | return False 134 | 135 | def get_real_room_id(self, room_id): 136 | if room_id.startswith("#"): 137 | room_id = self.client.api.get_room_id(room_id) 138 | return room_id 139 | 140 | def get_room_members(self, room_id): 141 | key = "get_room_members-%s" % room_id 142 | res = self.cache.get(key) 143 | if res: 144 | self.logger.debug("get_room_members (cached): %s" % (key)) 145 | return res 146 | res = self.call_api("get_room_members", 2, room_id) 147 | self.cache.set(key, res, self.cache_timeout) 148 | self.logger.debug("get_room_members (non cached): %s" % (key)) 149 | return res 150 | 151 | def refresh_room_members(self, room_id): 152 | key = "get_room_members-%s" % room_id 153 | self.cache.delete(key) 154 | self.logger.debug("refresh_room_members: %s" % (key)) 155 | 156 | def is_room_member(self, room_id, user_id): 157 | try: 158 | r = Room(self.client, room_id) 159 | room_members = [m.user_id for m in r.get_joined_members()] 160 | return user_id in room_members 161 | except Exception as e: 162 | self.logger.error("Error when fetching room members: %s" % e) 163 | return False 164 | return False 165 | 166 | def check_send_mail_allowed(self, send_to): 167 | def _(f, if_, else_): 168 | if f == 'all': 169 | return if_ 170 | if isinstance(f, list): 171 | for i in f: 172 | if '@' in i: # assumes is an explicit mailbox 173 | if send_to == i: 174 | return if_ 175 | else: # assumes is a domain 176 | if send_to.split('@')[1] == i: 177 | return if_ 178 | if not f: # None 179 | return else_ 180 | return else_ 181 | 182 | s = self.settings.get('mail', {}) 183 | p = s.get('to_policy', 'deny') 184 | f = s.get('to_policy_filter', 'all') 185 | 186 | if p == 'allow': 187 | return _(f, True, False) 188 | if p == 'deny': 189 | return _(f, False, True) 190 | return False 191 | 192 | def send_mail(self, message, send_to): 193 | s = self.settings.get('mail', {}) 194 | if not self.check_send_mail_allowed(send_to): 195 | raise MatrixBotError("Outgoing mails to %s are not allowed" % send_to) 196 | mail = MIMEText(message) 197 | mail['Subject'] = s.get('subject') 198 | mail['From'] = s.get('from') 199 | mail['To'] = send_to 200 | mail['Reply-To'] = send_to 201 | mail['Date'] = email.utils.formatdate() 202 | self.logger.debug("Send mail to %s from %s" % (s.get('to'), s.get('from'))) 203 | self.logger.debug("Send mail content: %s" % message) 204 | if self.settings['mail']['ssl']: 205 | smtp = smtplib.SMTP_SSL(s.get('host'), s.get('port'), 206 | context=ssl.create_default_context()) 207 | smtp.login(s.get('username'), s.get('password')) 208 | else: 209 | smtp = smtplib.SMTP(self.settings['mail']['host'], 210 | self.settings['mail']['port']) 211 | smtp.send_message(mail) 212 | 213 | def do_command(self, action, sender, room_id, command, attempts=3): 214 | """ 215 | action : The action to execute 216 | sender : The sender of the message 217 | room_id : The actual Id of the room where the message was sent. It is 218 | used as default target_room_id if there is not other room id as 219 | paramenter in the command 220 | command : Format: user: action [dryrun] #room:domain @user:domain [!group:domain] ... 221 | attempts: Maximum number of retries to execute the action 222 | """ 223 | if sender: 224 | sender = self.normalize_user_id(sender) 225 | 226 | # TODO: This should be a decorator 227 | if self.only_local_domain and not self.is_local_user_id(sender): 228 | self.logger.warning( 229 | "do_command is not allowed for external sender (%s)" % sender 230 | ) 231 | return 232 | 233 | # command: bot invite #room:domain @user:domain ... 234 | # [0] [1] [2] [3:] 235 | command_arg_list = command.split()[2:] 236 | dry_mode = False 237 | if ( 238 | len(command_arg_list) > 0 and 239 | command_arg_list[0] == "dryrun" 240 | ): 241 | dry_mode = True 242 | command_arg_list = command_arg_list[1:] 243 | target_room_id = room_id 244 | if ( 245 | len(command_arg_list) > 0 and 246 | ( 247 | command_arg_list[0].startswith('!') or 248 | command_arg_list[0].startswith('#') 249 | ) 250 | ): 251 | target_room_id = self.get_real_room_id(command_arg_list[0]) 252 | command_arg_list = command_arg_list[1:] 253 | 254 | if sender and not self.is_room_member(target_room_id, sender): 255 | msg = "%s is not allowed for not members (%s) of the room (%s)" % (action, sender, target_room_id) 256 | self.logger.warning(msg) 257 | self.send_private_message(sender, 258 | msg, 259 | room_id) 260 | return 261 | 262 | room_members = set(map(lambda x: x['user_id'], 263 | self.get_room_members(target_room_id)["chunk"])) 264 | 265 | if action == "invite_user": 266 | selected_users = set(self._get_selected_users(command_arg_list)).difference(room_members) 267 | self.refresh_room_members(target_room_id) 268 | if action == "kick_user": 269 | selected_users = set(self._get_selected_users(command_arg_list)).intersection(room_members) 270 | self.refresh_room_members(target_room_id) 271 | 272 | if dry_mode and sender: 273 | self.send_private_message( 274 | sender, 275 | "Simulated '%s' action in room '%s' over: %s" % ( 276 | action, 277 | target_room_id, 278 | utils.list_to_str(selected_users)), 279 | room_id) 280 | else: 281 | if len(selected_users) > 0: 282 | for user in selected_users: 283 | self.logger.info( 284 | " do_command (%s,%s,%s,dry_mode=%s)" % ( 285 | action, 286 | target_room_id, 287 | user, 288 | dry_mode)) 289 | res = self.call_api(action, attempts, target_room_id, user) 290 | if sender: 291 | msg = '''Action '%s' in room %s over %s''' % ( 292 | action, 293 | target_room_id, 294 | utils.list_to_str(selected_users) 295 | ) 296 | self.send_private_message(sender, msg, room_id) 297 | elif sender: 298 | self.send_private_message(sender, 299 | "No users found", 300 | room_id) 301 | 302 | def invite_subscriptions(self): 303 | for room_id in self.subscriptions_room_ids: 304 | command = self.username.lower() + ": invite " + self.settings["subscriptions"][room_id] 305 | self.do_command("invite_user", None, room_id, command, attempts=1) 306 | 307 | def kick_revokations(self): 308 | for room_id in self.revokations_rooms_ids: 309 | command = self.username.lower() + ": kick " + self.settings["revokations"][room_id] 310 | self.do_command("kick_user", None, room_id, command, attempts=1) 311 | 312 | def call_api(self, action, max_attempts, *args): 313 | method = getattr(self.client.api, action) 314 | attempts = max_attempts 315 | while attempts > 0: 316 | try: 317 | response = method(*args) 318 | self.logger.info("Call %s action with: %s" % (action, args)) 319 | self.logger.debug("Call response: %s" % (response)) 320 | return response 321 | except MatrixRequestError as e: 322 | self.logger.debug("Fail (%s/%s) in call %s action with: %s - %s" % (attempts, max_attempts, action, args, e)) 323 | attempts -= 1 324 | time.sleep(5) 325 | return None 326 | 327 | def send_emote(self, room_id, message): 328 | return self.call_api("send_emote", 3, 329 | room_id, message) 330 | 331 | def send_html(self, room_id, message, msgtype="m.text"): 332 | content = { 333 | "body": re.sub('<[^<]+?>', '', message), 334 | "msgtype": msgtype, 335 | "format": "org.matrix.custom.html", 336 | "formatted_body": message 337 | } 338 | return self.client.api.send_message_event( 339 | room_id, "m.room.message", 340 | content 341 | ) 342 | 343 | def send_message(self, room_id, message): 344 | return self.call_api("send_message", 3, 345 | room_id, message) 346 | 347 | def send_notice(self, room_id, message): 348 | return self.call_api("send_notice", 3, 349 | room_id, message) 350 | 351 | def send_private_message(self, user_id, message, room_id=None): 352 | # Just add a first case: if the channel is 1-to-1 then reply 353 | # directly using this channel 354 | if room_id and self.is_private_room(room_id, self.get_user_id(), user_id): 355 | return self.call_api("send_message", 3, room_id, message) 356 | 357 | user_room_id = self.get_private_room_with(user_id) 358 | if room_id and room_id != user_room_id: 359 | self.call_api( 360 | "send_message", 361 | 3, 362 | room_id, 363 | "Replying command as PM to %s" % user_id) 364 | return self.send_message(room_id, message) 365 | 366 | async def loop(self): 367 | loop_pool = [] 368 | loop_max = 10 369 | await self.sync(ignore=True) # Ignoring pending old messages 370 | while True: 371 | try: 372 | loop_pool.append(asyncio.ensure_future(self.sync())) 373 | await asyncio.sleep(self.period) 374 | if loop_pool == loop_max: 375 | while len(loop_pool) > 0: 376 | task = loop_pool.pop() 377 | try: 378 | await task 379 | except asyncio.CancelledError as e: 380 | self.logger.error("matrixbot: Sync cancelled: %s" % e) 381 | except Exception as e: 382 | self.logger.error("matrixbot: Unexpected error: %s" % e) 383 | self.logger.error("matrixbot: Unexpected error: %s" % traceback.print_exc()) 384 | 385 | def leave_empty_rooms(self): 386 | self.logger.debug("leave_empty_rooms") 387 | rooms = self.get_rooms() 388 | for room_id in rooms: 389 | res = self.get_room_members(room_id) 390 | try: 391 | members_list = res.get('chunk', []) 392 | except Exception as e: 393 | members_list = [] 394 | self.logger.debug("Error getting the list of members in room %s: %s" % (room_id, e)) 395 | 396 | if len(members_list) > 2: 397 | continue # We are looking for a 1-to-1 room 398 | 399 | for r in res.get('chunk', []): 400 | if 'user_id' in r and 'membership' in r: 401 | if r['membership'] == 'leave': 402 | self.call_api("kick_user", 1, room_id, self.get_user_id()) 403 | try: 404 | self.call_api("forget_room", 1, room_id) 405 | except Exception as e: 406 | self.logger.warning("Some kind of error during the forget_room action: %s" % (e)) 407 | 408 | def get_private_room_with(self, user_id): 409 | self.leave_empty_rooms() 410 | self.logger.debug("get_private_room_with") 411 | 412 | rooms = self.get_rooms() 413 | for room_id in rooms: 414 | if self.is_private_room(room_id, self.get_user_id(), user_id): 415 | return room_id 416 | 417 | # Not room found then ... 418 | room_id = self.call_api("create_room", 3, 419 | None, False, 420 | [user_id])['room_id'] 421 | self.call_api( 422 | "send_message", 423 | 3, 424 | room_id, 425 | "Hi! Get info about how to interact with me typing: %s help" % self.username 426 | ) 427 | return room_id 428 | 429 | def is_private_room(self, room_id, user1_id, user2_id=None): 430 | me = False # me is true if the user1_id is in the room 431 | him = False # him is true if the user2_id join or is already 432 | 433 | res = self.get_room_members(room_id) 434 | try: 435 | members_list = res.get('chunk', []) 436 | except Exception as e: 437 | members_list = [] 438 | self.logger.debug( 439 | "Error getting the members of the room %s: %s" % (room_id, e)) 440 | 441 | if len(members_list) != 2: 442 | self.logger.debug("Room %s is not a 1-to-1 room" % room_id) 443 | return False # We are looking for a 1-to-1 room 444 | 445 | if not user2_id: 446 | self.logger.debug("Room %s is a 1-to-1 with the user %s" % (room_id, user1_id)) 447 | return True # I just check if the room is 1-to-1 for user1_id 448 | 449 | 450 | # TODO: This code must be cleaned up 451 | for r in res.get('chunk', []): 452 | if ( 453 | 'content' in r 454 | and 'state_key' in r 455 | and 'membership' in r['content'] 456 | ): 457 | if r['state_key'] == user2_id and r['content']['membership'] == 'invite': 458 | him = True 459 | if r['state_key'] == user2_id and r['content']['membership'] == 'join': 460 | him = True 461 | if r['state_key'] == user1_id and r['content']['membership'] == 'join': 462 | me = True 463 | if me and him: 464 | self.logger.debug( 465 | "A 1-to-1 room for %s and %s found: %s" % ( 466 | user2_id, 467 | user1_id, 468 | room_id)) 469 | return True 470 | 471 | for r in res.get('chunk', []): 472 | if ( 473 | 'prev_content' in r 474 | and 'state_key' in r['prev_content'] 475 | and 'membership' in r['prev_content'] 476 | ): 477 | p = r['prev_content'] 478 | if p['state_key'] == user2_id and p['membership'] == 'invite': 479 | him = True 480 | if p['state_key'] == user2_id and p['membership'] == 'join': 481 | him = True 482 | if p['state_key'] == user1_id and p['membership'] == 'join': 483 | me = True 484 | if me and him: 485 | self.logger.debug( 486 | "A 1-to-1 room for %s and %s found: %s" % ( 487 | user2_id, 488 | user1_id, 489 | room_id)) 490 | return True 491 | return False 492 | 493 | def is_explicit_call(self, body): 494 | if ( 495 | body.lower().strip().startswith("%s:" % self.username.lower()) 496 | or body.lower().strip().startswith("%s " % self.username.lower()) 497 | ): 498 | return True 499 | res = False 500 | self.logger.debug("is_explicit_call: %s" % res) 501 | return res 502 | 503 | def is_command(self, body, command="command_name"): 504 | res = False 505 | if self.is_explicit_call(body): 506 | command_list = body.split()[1:] 507 | if len(command_list) == 0: 508 | if command == "help": 509 | res = True 510 | else: 511 | if command_list[0] == command: 512 | res = True 513 | self.logger.debug("is_%s: %s" % (command, res)) 514 | return res 515 | 516 | def join_rooms(self, silent=True): 517 | for room_id in self.room_ids: 518 | try: 519 | room = self.client.join_room(room_id) 520 | room_id = room.room_id # Ensure we are using the actual id not the alias 521 | if not silent: 522 | self.send_message(room_id, "Mornings!") 523 | except MatrixRequestError as e: 524 | self.logger.error("Join action in room %s failed: %s" % 525 | (room_id, e)) 526 | 527 | new_subscriptions_room_ids = [] 528 | for room_id in list(self.subscriptions_room_ids): 529 | try: 530 | old_room_id = room_id 531 | room_id = room_id + ':' + self.domain 532 | room = self.client.join_room(room_id) 533 | new_room_id = room.room_id # Ensure we are using the actual id not the alias 534 | new_subscriptions_room_ids.append(new_room_id) 535 | self.settings["subscriptions"][new_room_id] = self.settings["subscriptions"][old_room_id] 536 | except MatrixRequestError as e: 537 | self.logger.error("Join action for subscribe users in room %s failed: %s" % 538 | (room_id, e)) 539 | self.subscriptions_room_ids = new_subscriptions_room_ids 540 | 541 | new_revokations_room_ids = [] 542 | for room_id in list(self.revokations_rooms_ids): 543 | try: 544 | old_room_id = room_id 545 | room_id = room_id + ':' + self.domain 546 | room = self.client.join_room(room_id) 547 | new_room_id = room.room_id # Ensure we are using the actual id not the alias 548 | new_revokations_room_ids.append(new_room_id) 549 | self.settings["revokations"][new_room_id] = self.settings["revokations"][old_room_id] 550 | except MatrixRequestError as e: 551 | self.logger.error("Join action for revoke users in room %s failed: %s" % 552 | (room_id, e)) 553 | self.revokations_rooms_ids = new_revokations_room_ids 554 | 555 | def do_join(self, sender, room_id, body): 556 | self.logger.debug("do_join") 557 | 558 | # TODO: This should be a decorator 559 | if self.only_local_domain and not self.is_local_user_id(sender): 560 | self.logger.warning( 561 | "do_join is not allowed for external sender (%s)" % sender 562 | ) 563 | return 564 | 565 | body_arg_list = body.split()[2:] 566 | dry_mode = False 567 | msg_dry_mode = " (dryrun)" if dry_mode else "" 568 | 569 | if len(body_arg_list) > 0 and body_arg_list[0] == "dryrun": 570 | dry_mode = True 571 | body_arg_list = body.split()[3:] 572 | original_room_id = body_arg_list[0] 573 | join_room_id = body_arg_list[0] 574 | 575 | # If the user did not specify a domain, try to append our 576 | # domain to the room that they passed us. 577 | domain_suffix = ":%s" % self.domain 578 | if not ":" in join_room_id: 579 | join_room_id += domain_suffix 580 | 581 | if not join_room_id.endswith(domain_suffix): 582 | msg = '''Invalid room id (%s): Join is only for rooms in %s domain''' % (join_room_id, self.domain) 583 | self.send_private_message(sender, msg, room_id) 584 | return 585 | 586 | if not join_room_id.startswith("#"): 587 | msg = '''Invalid room id (%s): Join is only valid using room aliases''' % (join_room_id) 588 | self.send_private_message(sender, msg, room_id) 589 | return 590 | 591 | try: 592 | join_room_id = self.get_real_room_id(join_room_id) 593 | except Exception as e: 594 | msg = '''Room not %s found: %s''' % (join_room_id, e) 595 | self.send_private_message(sender, msg, room_id) 596 | self.logger.warning(msg) 597 | return 598 | 599 | allowed_users = self.default_allowed_join_rooms 600 | original_room_name = original_room_id.split(":")[0] 601 | if (original_room_name in self.allowed_join_rooms_ids): 602 | allowed_users = self.settings["allowed-join"][original_room_name] 603 | 604 | selected_users = self._get_selected_users(allowed_users.split()) 605 | 606 | self.logger.debug("Checking if %s is in %s" % (sender, selected_users)) 607 | if sender not in selected_users: 608 | msg = '''User %s can't join in room %s''' % (sender, original_room_id) + msg_dry_mode 609 | self.send_private_message(sender, msg, room_id) 610 | return 611 | 612 | try: 613 | if not dry_mode: 614 | self.logger.info( 615 | "do_join (%s,%s)" % ( 616 | join_room_id, 617 | sender 618 | ) 619 | ) 620 | res = self.call_api("invite_user", 3, join_room_id, sender) 621 | if type(res) == dict: 622 | msg_ok = '''Invitation sent to user %s to join in %s%s''' % ( 623 | sender, 624 | original_room_id, 625 | msg_dry_mode) 626 | self.send_private_message(sender, msg_ok, room_id) 627 | else: 628 | msg_fail = '''Fail in invitation sent to user %s to join in %s%s: %s''' % ( 629 | sender, 630 | original_room_id, 631 | msg_dry_mode, 632 | res) 633 | self.send_private_message(sender, msg_fail, room_id) 634 | except MatrixRequestError as e: 635 | self.logger.warning(e) 636 | 637 | def do_list_groups(self, sender, room_id): 638 | self.logger.debug("do_list_groups") 639 | # TODO: This should be a decorator 640 | if self.only_local_domain and not self.is_local_user_id(sender): 641 | self.logger.warning( 642 | "do_list_groups is not allowed for external sender (%s)" % sender 643 | ) 644 | return 645 | 646 | groups = ', '.join(["+%s" % x for x in self.settings["ldap"]["groups"]]) 647 | try: 648 | msg = "Groups: %s" % groups 649 | self.send_private_message(sender, msg, room_id) 650 | except MatrixRequestError as e: 651 | self.logger.warning(e) 652 | 653 | def do_list_rooms(self, sender, room_id): 654 | self.logger.debug("do_list_rooms") 655 | # TODO: This should be a decorator 656 | if self.only_local_domain and not self.is_local_user_id(sender): 657 | self.logger.warning( 658 | "do_list_rooms is not allowed for external sender (%s)" % sender 659 | ) 660 | return 661 | is_super_user = sender in self.super_users 662 | msg = "" 663 | if is_super_user: 664 | msg = "[*] Your are a super-user" 665 | if self.enable_list_rooms_commands: 666 | msg = msg + ". Showing full list of rooms.\n\n" 667 | else: 668 | msg = msg + " but super-user support for this command is DISABLED.\n\n" 669 | msg = msg + "Room list:\n" 670 | rooms = self.get_rooms() 671 | rooms_msg_list = [] 672 | for r in rooms: 673 | aliases = self.get_room_aliases(r) 674 | if len(aliases) < 1: 675 | self.logger.debug("Room %s hasn't got aliases. Skipping" % (r)) 676 | continue # We are looking for rooms with alias 677 | try: 678 | name = self.client.api.get_room_name(r)['name'] 679 | except Exception as e: 680 | self.logger.debug("Error getting the room name %s: %s" % (r, e)) 681 | name = "No named" 682 | if self.enable_list_rooms_commands: 683 | is_visible_room = False 684 | for alias in self.visible_subset_list_rooms_commands: 685 | if alias in aliases: 686 | is_visible_room = True 687 | break 688 | if not is_visible_room and not is_super_user: 689 | continue 690 | status = "" 691 | if self.enable_list_rooms_commands and is_super_user: 692 | if is_visible_room: 693 | status = " [visible]" 694 | else: 695 | status = " [hidden]" 696 | rooms_msg_list.append("* %s - %s%s" % (name, "".join(aliases), status)) 697 | msg += "\n".join(sorted(rooms_msg_list)) 698 | try: 699 | self.send_private_message(sender, msg, room_id) 700 | except MatrixRequestError as e: 701 | self.logger.warning(e) 702 | 703 | def do_list(self, sender, room_id, body): 704 | self.logger.debug("do_list") 705 | # TODO: This should be a decorator 706 | if self.only_local_domain and not self.is_local_user_id(sender): 707 | self.logger.warning( 708 | "do_list is not allowed for external sender (%s)" % sender 709 | ) 710 | return 711 | 712 | body_arg_list = body.split()[2:] 713 | selected_users = self._get_selected_users(body_arg_list) 714 | msg_list = " ".join( 715 | [self.normalize_user_id(x) for x in selected_users] 716 | ) 717 | try: 718 | self.send_private_message(sender, msg_list, room_id) 719 | except MatrixRequestError as e: 720 | self.logger.warning(e) 721 | 722 | def do_count(self, sender, room_id, body): 723 | # TODO: This should be a decorator 724 | if self.only_local_domain and not self.is_local_user_id(sender): 725 | self.logger.warning( 726 | "do_count is not allowed for external sender (%s)" % sender 727 | ) 728 | return 729 | 730 | self.logger.debug("do_count") 731 | body_arg_list = body.split()[2:] 732 | selected_users = self._get_selected_users(body_arg_list) 733 | msg_list = "Count: %s" % len(selected_users) 734 | try: 735 | self.send_private_message(sender, msg_list, room_id) 736 | except MatrixRequestError as e: 737 | self.logger.warning(e) 738 | 739 | def do_help(self, sender, room_id, body, pm=False): 740 | vars_ = self.settings["matrix"].copy() 741 | if pm: 742 | vars_["prefix"] = "" 743 | else: 744 | vars_["prefix"] = "%(username)s: " % vars_ 745 | 746 | vars_["aliases"] = "\n".join(["%s: " % vars_["username"] + "%s ==> %s" % x for x in list(utils.get_aliases(self.settings).items())]) 747 | try: 748 | self.logger.debug("do_help") 749 | msg_help = '''Examples: 750 | %(prefix)shelp 751 | %(prefix)shelp extra 752 | %(prefix)sjoin 753 | %(prefix)sinvite [dryrun] [] (@user|+group) ... [ but (@user|+group) ] 754 | %(prefix)skick [dryrun] [] (@user|+group) ... [ but (@user|+group) ] 755 | %(prefix)scount [ (@user|+group) ... [ but (@user|+group) ] ] 756 | %(prefix)slist [ (@user|+group) ... [ but (@user|+group) ] ] 757 | %(prefix)slist-rooms 758 | %(prefix)slist-groups 759 | %(prefix)sforward-to-email mailbox@example.domain (as reply for a message) 760 | ''' % vars_ 761 | if body.find("extra") >= 0: 762 | msg_help += ''' 763 | Available command aliases: 764 | 765 | %(aliases)s 766 | ''' % vars_ 767 | 768 | # TODO: This should be a decorator ??? 769 | if self.only_local_domain and not self.is_local_user_id(sender): 770 | self.logger.warning( 771 | "do_help is not allowed for external sender (%s)" % sender 772 | ) 773 | else: 774 | self.send_private_message(sender, msg_help, room_id) 775 | 776 | # get plugin help to plugins 777 | for plugin in self.plugins: 778 | plugin.help( 779 | sender, 780 | room_id, 781 | lambda r,m: self.send_private_message(sender, m, None) 782 | ) 783 | 784 | except MatrixRequestError as e: 785 | self.logger.warning(e) 786 | 787 | def do_forward_to_email(self, sender, room_id, body, in_reply_to=None): 788 | self.logger.debug("do_forward_to_email") 789 | # TODO: This should be a decorator 790 | if self.only_local_domain and not self.is_local_user_id(sender): 791 | self.logger.warning( 792 | "do_forward_to_email is not allowed for external sender (%s)" % sender 793 | ) 794 | return 795 | 796 | if not in_reply_to: 797 | msg = "🙋‍♀️: The forward-to-email only works as reply of a previous message" 798 | try: 799 | self.send_private_message(sender, msg, room_id) 800 | except MatrixRequestError as e: 801 | self.logger.warning(e) 802 | return 803 | 804 | body_arg_list = body.split()[2:] 805 | send_to = "" 806 | if (len(body_arg_list) > 0): 807 | send_to = body_arg_list[0] 808 | 809 | if not send_to: 810 | msg = "🙋‍♀️: The forward-to-email command has to be completed with a mail destination" 811 | try: 812 | self.send_private_message(sender, msg, room_id) 813 | except MatrixRequestError as e: 814 | self.logger.warning(e) 815 | return 816 | messages = [] 817 | replies = {} 818 | end = False 819 | token = self.sync_token 820 | max_iters = 10 821 | for i in range(max_iters): 822 | r = self.call_api("get_room_messages", 1, room_id, token, "b", 500) 823 | for c in r["chunk"]: 824 | command = self._get_command(room_id, c) 825 | if self.is_command(command, "forward-to-email"): 826 | continue # 'forward-to-email' are delivery skipped 827 | if c.get("type", "") == "m.room.message": 828 | c_in_reply_to = utils.get_in_reply_to(c) 829 | if c_in_reply_to: 830 | if c_in_reply_to in replies: 831 | replies[c_in_reply_to].append(c) 832 | else: 833 | replies[c_in_reply_to]= [c] 834 | else: 835 | messages.append(c) 836 | if c['event_id'] == in_reply_to: 837 | end = True 838 | break 839 | if end: 840 | break 841 | token = r["end"] 842 | 843 | content = "" 844 | for message in reversed(messages): 845 | content += utils.mail_format_event(message, replies) 846 | 847 | aliases = self.get_room_aliases(room_id) 848 | message = ''' 849 | Thread forwarded from %s, 850 | 851 | - 8< ---------------------------------------------------------------- 852 | 853 | %s 854 | - 8< ---------------------------------------------------------------- 855 | 856 | ''' % (aliases if aliases else room_id, content) 857 | 858 | msg = "Messages forwarded to %s" % send_to 859 | 860 | try: 861 | self.send_mail(message, send_to) 862 | except MatrixBotError as e: 863 | msg = "🙋‍♀️: Messages can not be forwarded: %s" % (e) 864 | self.logger.error("matrixbot: error: %s" % e) 865 | traceback.print_exc() 866 | except Exception as e: 867 | msg = "🙋‍♀️: Messages can not be forwarded to %s: %s" % (send_to, e) 868 | self.logger.error("matrixbot: error: %s" % e) 869 | traceback.print_exc() 870 | 871 | try: 872 | self.send_message(room_id, msg) 873 | except MatrixRequestError as e: 874 | self.logger.warning(e) 875 | 876 | def _set_rooms(self, response_dict): 877 | new_room_list = [] 878 | for rooms_types in list(response_dict['rooms'].keys()): 879 | for room_id in list(response_dict['rooms'][rooms_types].keys()): 880 | new_room_list.append(room_id) 881 | self._set_room_aliases(room_id) 882 | self.rooms = new_room_list 883 | 884 | def _set_room_aliases(self, room_id): 885 | room_dict_state = None 886 | try: 887 | aliases = [] 888 | room_dict_state = self.client.api.get_room_state(room_id) 889 | for e in room_dict_state: 890 | if e['type'] == 'm.room.canonical_alias': 891 | aliases = e['content']['alias'] 892 | self.room_aliases[room_id] = aliases 893 | except Exception as e: 894 | self.logger.log(EXTRA_DEBUG, "Error getting aliases for %s: %s" % (room_id, e)) 895 | self.logger.log(EXTRA_DEBUG, "Dict: %s" % (room_dict_state)) 896 | 897 | def get_rooms(self): 898 | return self.rooms 899 | 900 | def get_room_aliases(self, room_id): 901 | return self.room_aliases[room_id] if room_id in self.room_aliases else [] 902 | 903 | async def _dispatch(self, response): 904 | _tasks = [] 905 | 906 | if not response: 907 | return 908 | 909 | async def _(plugin, callback): 910 | try: 911 | plugin.dispatch(callback) 912 | except Exception as e: 913 | self.logger.error( 914 | "Error in plugin %s: %s" % (plugin.name, e) 915 | ) 916 | 917 | for plugin in self.plugins: 918 | _tasks.append(asyncio.create_task(_(plugin, self.send_message))) 919 | _tasks.append(asyncio.ensure_future( 920 | self.sync_invitations(response['rooms'].get('invite', {})))) 921 | _tasks.append(asyncio.ensure_future( 922 | self.sync_joins(response['rooms'].get('join', {})))) 923 | for task in _tasks: 924 | await task 925 | 926 | async def sync(self, ignore=False, timeout_ms=30000): 927 | response = None 928 | try: 929 | response = self.client.api.sync(self.sync_token, timeout_ms, full_state=True) 930 | self._set_rooms(response) 931 | self.sync_token = response["next_batch"] 932 | self.logger.info("!!! sync_token: %s" % (self.sync_token)) 933 | self.logger.log(EXTRA_DEBUG, "Sync response: %s" % (response)) 934 | except Exception as e: 935 | self.logger.error("Error in sync: %s" % e) 936 | if not ignore: 937 | await self._dispatch(response) 938 | 939 | async def sync_invitations(self, invite_events): 940 | _tasks = [] 941 | # TODO Clean code and also use only_local_domain setting 942 | for room_id, invite_state in list(invite_events.items()): 943 | self.logger.info("+++ (invite) %s" % (room_id)) 944 | for event in invite_state["invite_state"]["events"]: 945 | if event["type"] == 'm.room.member' and \ 946 | "content" in event and \ 947 | "membership" in event["content"] and \ 948 | event["content"]["membership"] == 'invite' and \ 949 | "sender" in event and \ 950 | event["sender"].endswith(self.domain): 951 | _tasks.append(asyncio.create_task( 952 | self.call_api("join_room", 3, room_id) 953 | )) 954 | for task in _tasks: 955 | await task 956 | 957 | 958 | async def sync_joins(self, join_events): 959 | _tasks = [] 960 | for room_id, sync_room in list(join_events.items()): 961 | self.logger.debug(">>> (join) %s" % (room_id)) 962 | for event in sync_room["timeline"]["events"]: 963 | _tasks.append(asyncio.ensure_future( 964 | self._process_event(room_id, event) 965 | )) 966 | for task in _tasks: 967 | await task 968 | 969 | def _get_command(self, room_id, event): 970 | body = event["content"]["body"] 971 | if utils.is_reply(event): 972 | body = "\n\n".join(body.split("\n\n")[1:]) 973 | 974 | is_pm = self.is_private_room(room_id, self.get_user_id()) 975 | 976 | if is_pm and not self.is_explicit_call(body): 977 | body = "%s: " % self.username.lower() + body 978 | 979 | body = utils.get_command_alias(body, self.settings) 980 | return body 981 | 982 | async def _process_event(self, room_id, event): 983 | if not ( 984 | event["type"] == 'm.room.message' 985 | and "content" in event 986 | and "msgtype" in event["content"] 987 | and event["content"]["msgtype"] == 'm.text' 988 | ): 989 | return 990 | 991 | if self.commands_enable: 992 | sender = event["sender"] 993 | is_pm = self.is_private_room(room_id, self.get_user_id()) 994 | in_reply_to = utils.get_in_reply_to(event) 995 | command = self._get_command(room_id, event) 996 | 997 | if sender == self.get_user_id(): 998 | return 999 | 1000 | if not command.lower().strip().startswith("%s" % self.username): 1001 | return 1002 | if self.is_command(command, "invite"): 1003 | self.do_command("invite_user", sender, room_id, command) 1004 | elif self.is_command(command, "kick"): 1005 | self.do_command("kick_user", sender, room_id, command) 1006 | elif self.is_command(command, "join"): 1007 | self.do_join(sender, room_id, command) 1008 | elif self.is_command(command, "count"): 1009 | self.do_count(sender, room_id, command) 1010 | elif self.is_command(command, "list"): 1011 | self.do_list(sender, room_id, command) 1012 | elif self.is_command(command, "list-rooms"): 1013 | self.do_list_rooms(sender, room_id) 1014 | elif self.is_command(command, "list-groups"): 1015 | self.do_list_groups(sender, room_id) 1016 | elif self.is_command(command, "forward-to-email"): 1017 | self.do_forward_to_email(sender, room_id, command, in_reply_to) 1018 | elif self.is_command(command, "help"): 1019 | self.do_help(sender, room_id, command, is_pm) 1020 | elif len (command.split()[1:]) == 0 : 1021 | self.do_help(sender, room_id, command, is_pm) 1022 | 1023 | # push to plugins 1024 | for plugin in self.plugins: 1025 | plugin.command( 1026 | sender, room_id, command, 1027 | self.send_message 1028 | ) 1029 | --------------------------------------------------------------------------------