├── MANIFEST.in ├── setup.cfg ├── .travis.yml ├── update_emoji_map.py ├── LICENSE ├── setup.py ├── .gitignore ├── README.rst ├── tests └── payload_test.py └── mattersend.py /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | with-coverage=1 3 | cover-package=mattersend 4 | cover-html=1 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 3.5 4 | install: pip install . pyfakefs coverage coveralls 5 | script: nosetests 6 | after_success: coveralls 7 | -------------------------------------------------------------------------------- /update_emoji_map.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | response = requests.get('https://raw.githubusercontent.com/mattermost/platform/master/webapp/utils/emoji.json').json() 4 | 5 | with open('mattersend.py', 'r') as f: 6 | mattersend_py = f.readlines() 7 | 8 | updated_mattersend_py = [] 9 | 10 | in_block = False 11 | for line in mattersend_py: 12 | if not in_block: 13 | updated_mattersend_py.append(line) 14 | 15 | if line == 'emoji_to_code = {\n': 16 | in_block = True 17 | 18 | for icondata in response: 19 | name = icondata[1]['name'] 20 | code = icondata[1].get('unicode', name) 21 | updated_mattersend_py.append(" '{0}': '{1}',\n".format(name, code)) 22 | 23 | elif line == '}\n': 24 | updated_mattersend_py.append(line) 25 | in_block = False 26 | 27 | with open('mattersend.py', 'w') as f: 28 | f.write(''.join(updated_mattersend_py)) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | © 2015 Massimiliano Torromeo 2 | 3 | This program is distributed under the terms of the BSD License. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions 7 | are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 16 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | import os 5 | import mattersend 6 | from setuptools import setup 7 | 8 | README = os.path.realpath(os.path.sep.join([__file__, os.path.pardir, 'README.rst'])) 9 | 10 | setup( 11 | name=mattersend.name, 12 | py_modules=['mattersend'], 13 | entry_points={ 14 | 'console_scripts': [ 15 | 'mattersend=mattersend:main', 16 | ], 17 | }, 18 | install_requires=["setproctitle", "requests"], 19 | tests_require=["nose", "coverage", "pyfakefs"], 20 | test_suite="nose.collector", 21 | version=mattersend.version, 22 | description=mattersend.description, 23 | long_description=open(README, 'r').read(), 24 | author="Massimiliano Torromeo", 25 | author_email="massimiliano.torromeo@gmail.com", 26 | url=mattersend.url, 27 | download_url="{}/tarball/v{}".format(mattersend.url, mattersend.version), 28 | classifiers=[ 29 | "Programming Language :: Python", 30 | "Programming Language :: Python :: 2.7", 31 | "Programming Language :: Python :: 3.4", 32 | "License :: OSI Approved :: MIT License", 33 | "Intended Audience :: System Administrators", 34 | "Operating System :: POSIX :: Linux", 35 | "Natural Language :: English", 36 | "Topic :: Utilities" 37 | ], 38 | license="MIT License" 39 | ) 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /cover 2 | /env2 3 | 4 | # Created by https://www.gitignore.io/api/linux,python,kate 5 | 6 | ### Linux ### 7 | *~ 8 | 9 | # temporary files which can be created if a process still has a handle open of a deleted file 10 | .fuse_hidden* 11 | 12 | # KDE directory preferences 13 | .directory 14 | 15 | # Linux trash folder which might appear on any partition or disk 16 | .Trash-* 17 | 18 | 19 | ### Python ### 20 | # Byte-compiled / optimized / DLL files 21 | __pycache__/ 22 | *.py[cod] 23 | *$py.class 24 | 25 | # C extensions 26 | *.so 27 | 28 | # Distribution / packaging 29 | .Python 30 | env/ 31 | build/ 32 | develop-eggs/ 33 | dist/ 34 | downloads/ 35 | eggs/ 36 | .eggs/ 37 | lib/ 38 | lib64/ 39 | parts/ 40 | sdist/ 41 | var/ 42 | *.egg-info/ 43 | .installed.cfg 44 | *.egg 45 | 46 | # PyInstaller 47 | # Usually these files are written by a python script from a template 48 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 49 | *.manifest 50 | *.spec 51 | 52 | # Installer logs 53 | pip-log.txt 54 | pip-delete-this-directory.txt 55 | 56 | # Unit test / coverage reports 57 | htmlcov/ 58 | .tox/ 59 | .coverage 60 | .coverage.* 61 | .cache 62 | nosetests.xml 63 | coverage.xml 64 | *,cover 65 | .hypothesis/ 66 | 67 | # Translations 68 | *.mo 69 | *.pot 70 | 71 | # Django stuff: 72 | *.log 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | #Ipython Notebook 81 | .ipynb_checkpoints 82 | 83 | 84 | ### Kate ### 85 | # Swap Files # 86 | .*.kate-swp 87 | .swp.* 88 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | mattersend |build-status| |cover-status| 2 | ======================================== 3 | 4 | CLI tool to send messages to the Incoming webhook of mattermost (http://www.mattermost.org/). 5 | 6 | Help 7 | ---- 8 | 9 | :: 10 | 11 | usage: mattersend [-h] [-V] [-C CONFIG] [-s SECTION] [-c CHANNEL] [-U URL] 12 | [-u USERNAME] [-i ICON] [-t [DIALECT] | -y SYNTAX] [-n] 13 | [-f FILE] 14 | 15 | Sends messages to mattermost's incoming webhooks via CLI 16 | 17 | optional arguments: 18 | -h, --help show this help message and exit 19 | -V, --version show program's version number and exit 20 | -C CONFIG, --config CONFIG 21 | Use a different configuration file 22 | -s SECTION, --section SECTION 23 | Configuration file section 24 | -c CHANNEL, --channel CHANNEL 25 | Send to this channel or @username 26 | -U URL, --url URL Mattermost webhook URL 27 | -u USERNAME, --username USERNAME 28 | Username 29 | -i ICON, --icon ICON Icon 30 | -t [DIALECT], --tabular [DIALECT] 31 | Parse input as CSV and format it as a table (DIALECT 32 | can be one of sniff, excel, excel-tab, unix) 33 | -y SYNTAX, --syntax SYNTAX 34 | -n, --dry-run, --just-print 35 | Don't send, just print the payload 36 | -f FILE, --file FILE Read content from FILE. If - reads from standard input 37 | (DEFAULT: -) 38 | 39 | Configuration file 40 | ------------------ 41 | 42 | The only required option to start sending messages to mattermost is the webhook url. 43 | You can either set this in a configurations file (globally in */etc/mattersend.conf* or locally in *$HOME/.mattersend.conf*) or specify it on the CLI with the --url argument. 44 | 45 | You can have multiple sections to override DEFAULT settings and later select them from the CLI with the --section argument. 46 | 47 | This is an example of a configuration file for mattersend:: 48 | 49 | [DEFAULT] 50 | url = https://mattermost.example.com/hooks/XXXXXXXXXXXXXXXXXXXXXXX 51 | icon = :ghost: 52 | username = This is a bot 53 | channel = @myself 54 | 55 | [angrybot] 56 | icon = :angry: 57 | username = AngryBot 58 | 59 | Example usage 60 | ------------- 61 | 62 | :: 63 | 64 | echo "Hello world!" | mattersend -U https://mattermost.example.com/hooks/XXX 65 | 66 | # you can omit -U with mattersend.conf 67 | echo "Hello world!" | mattersend 68 | 69 | # send file content 70 | mattersend -f todo.txt 71 | 72 | # table data 73 | echo -e "ABC;DEF;GHI\nfoo;bar;baz" | mattersend -t 74 | 75 | LICENSE 76 | ------- 77 | Copyright (c) 2016 Massimiliano Torromeo 78 | 79 | mattersend is free software released under the terms of the BSD license. 80 | 81 | See the LICENSE file provided with the source distribution for full details. 82 | 83 | Contacts 84 | -------- 85 | 86 | * Massimiliano Torromeo 87 | 88 | .. |build-status| image:: https://travis-ci.org/mtorromeo/mattersend.svg?branch=master 89 | :target: https://travis-ci.org/mtorromeo/mattersend 90 | :alt: Build status 91 | .. |cover-status| image:: https://coveralls.io/repos/github/mtorromeo/mattersend/badge.svg?branch=master 92 | :target: https://coveralls.io/github/mtorromeo/mattersend?branch=master 93 | -------------------------------------------------------------------------------- /tests/payload_test.py: -------------------------------------------------------------------------------- 1 | import re 2 | import mattersend 3 | from pyfakefs import fake_filesystem_unittest 4 | 5 | try: 6 | from unittest import mock 7 | except ImportError: 8 | import mock 9 | 10 | 11 | def normalize_payload(payload): 12 | lines = [] 13 | for line in payload.splitlines(): 14 | lines.append(line.rstrip()) 15 | return "\n".join(lines) 16 | 17 | 18 | class MockResponse: 19 | def __init__(self, url, data): 20 | self.text = 'test' 21 | if url.endswith('/fail'): 22 | self.status_code = 502 23 | 24 | 25 | class PayloadTest(fake_filesystem_unittest.TestCase): 26 | def setUp(self): 27 | self.setUpPyfakefs() 28 | self.fs.CreateFile('/etc/mattersend.conf', contents='''[DEFAULT] 29 | url=https://chat.mydomain.com/hooks/abcdefghi123456 30 | 31 | [angrybot] 32 | icon = :angry: 33 | username = AngryBot''') 34 | self.fs.CreateFile('/etc/mime.types', contents='text/x-diff diff') 35 | self.fs.CreateFile('/home/test/source.coffee', contents='x' * 5000) 36 | self.fs.CreateFile('/home/test/source.csv', 37 | contents='abc,def\nfoo,bar') 38 | self.fs.CreateFile('/home/test/source.diff') 39 | self.fs.CreateFile('/home/test/Makefile') 40 | self.maxDiff = 20000 41 | 42 | def test_simple_1(self): 43 | payload = mattersend.send(channel='town-square', 44 | message='test message', 45 | just_return=True) 46 | 47 | self.assertEqual(normalize_payload(payload), 48 | r"""POST https://chat.mydomain.com/hooks/abcdefghi123456 49 | { 50 | "channel": "town-square", 51 | "text": "test message" 52 | }""") 53 | 54 | def test_section(self): 55 | payload = mattersend.send(channel='town-square', 56 | message='test message', 57 | config_section='angrybot', 58 | just_return=True) 59 | 60 | self.assertEqual(normalize_payload(payload), 61 | r"""POST https://chat.mydomain.com/hooks/abcdefghi123456 62 | { 63 | "channel": "town-square", 64 | "icon_url": "https://chat.mydomain.com/static/emoji/1f620.png", 65 | "text": "test message", 66 | "username": "AngryBot" 67 | }""") 68 | 69 | def test_override_url(self): 70 | payload = mattersend.send(channel='town-square', 71 | message='test message', 72 | url='http://chat.net/hooks/abdegh12', 73 | just_return=True) 74 | 75 | self.assertEqual(normalize_payload(payload), 76 | r"""POST http://chat.net/hooks/abdegh12 77 | { 78 | "channel": "town-square", 79 | "text": "test message" 80 | }""") 81 | 82 | def test_syntax_by_ext(self): 83 | payload = mattersend.send(channel='town-square', 84 | filename='/home/test/source.coffee', 85 | just_return=True) 86 | 87 | content = r"```coffeescript\n%s```" % ('x' * 5000,) 88 | self.assertEqual(normalize_payload(payload), 89 | r"""POST https://chat.mydomain.com/hooks/abcdefghi123456 90 | { 91 | "attachments": [ 92 | { 93 | "fallback": "%s", 94 | "text": "%s", 95 | "title": "source.coffee" 96 | } 97 | ], 98 | "channel": "town-square", 99 | "text": "" 100 | }""" % (content[:3501], content[:3501])) 101 | 102 | def test_syntax_by_mime(self): 103 | payload = mattersend.send(channel='town-square', 104 | filename='/home/test/source.diff', 105 | just_return=True) 106 | 107 | self.assertEqual(normalize_payload(payload), 108 | r"""POST https://chat.mydomain.com/hooks/abcdefghi123456 109 | { 110 | "attachments": [ 111 | { 112 | "fallback": "```diff\n```", 113 | "text": "```diff\n```", 114 | "title": "source.diff" 115 | } 116 | ], 117 | "channel": "town-square", 118 | "text": "" 119 | }""") 120 | 121 | def test_syntax_mk(self): 122 | payload = mattersend.send(channel='town-square', 123 | filename='/home/test/Makefile', 124 | just_return=True) 125 | 126 | self.assertEqual(normalize_payload(payload), 127 | r"""POST https://chat.mydomain.com/hooks/abcdefghi123456 128 | { 129 | "attachments": [ 130 | { 131 | "fallback": "```makefile\n```", 132 | "text": "```makefile\n```", 133 | "title": "Makefile" 134 | } 135 | ], 136 | "channel": "town-square", 137 | "text": "" 138 | }""") 139 | 140 | def test_filename_and_message(self): 141 | payload = mattersend.send(channel='town-square', 142 | filename='/etc/mime.types', 143 | message='test message', 144 | just_return=True) 145 | 146 | self.assertEqual(normalize_payload(payload), 147 | r"""POST https://chat.mydomain.com/hooks/abcdefghi123456 148 | { 149 | "attachments": [ 150 | { 151 | "fallback": "text/x-diff diff", 152 | "text": "text/x-diff diff", 153 | "title": "mime.types" 154 | } 155 | ], 156 | "channel": "town-square", 157 | "text": "test message" 158 | }""") 159 | 160 | def test_fileinfo(self): 161 | payload = mattersend.send(channel='town-square', 162 | filename='/home/test/source.coffee', 163 | fileinfo=True, 164 | just_return=True) 165 | 166 | content = r"```coffeescript\n%s```" % ('x' * 5000,) 167 | self.assertEqual(normalize_payload(payload), 168 | r"""POST https://chat.mydomain.com/hooks/abcdefghi123456 169 | { 170 | "attachments": [ 171 | { 172 | "fallback": "%s", 173 | "fields": [ 174 | { 175 | "short": true, 176 | "title": "Size", 177 | "value": "4.9KiB" 178 | }, 179 | { 180 | "short": true, 181 | "title": "Mime", 182 | "value": "None" 183 | } 184 | ], 185 | "text": "%s", 186 | "title": "source.coffee" 187 | } 188 | ], 189 | "channel": "town-square", 190 | "text": "" 191 | }""" % (content[:3501], content[:3501])) 192 | 193 | def test_csv(self): 194 | payload = mattersend.send(channel='town-square', 195 | filename='/home/test/source.csv', 196 | tabular='sniff', 197 | just_return=True) 198 | 199 | self.assertEqual(normalize_payload(payload), 200 | r"""POST https://chat.mydomain.com/hooks/abcdefghi123456 201 | { 202 | "attachments": [ 203 | { 204 | "fallback": "| abc | def |\n| --- | --- |\n| foo | bar |", 205 | "text": "| abc | def |\n| --- | --- |\n| foo | bar |", 206 | "title": "source.csv" 207 | } 208 | ], 209 | "channel": "town-square", 210 | "text": "" 211 | }""") 212 | 213 | def test_csv_dialect(self): 214 | payload = mattersend.send(channel='town-square', 215 | filename='/home/test/source.csv', 216 | tabular='excel', 217 | just_return=True) 218 | 219 | self.assertEqual(normalize_payload(payload), 220 | r"""POST https://chat.mydomain.com/hooks/abcdefghi123456 221 | { 222 | "attachments": [ 223 | { 224 | "fallback": "| abc | def |\n| --- | --- |\n| foo | bar |", 225 | "text": "| abc | def |\n| --- | --- |\n| foo | bar |", 226 | "title": "source.csv" 227 | } 228 | ], 229 | "channel": "town-square", 230 | "text": "" 231 | }""") 232 | 233 | @mock.patch('requests.post', side_effect=MockResponse) 234 | def test_send(self, mock_post): 235 | payload = mattersend.send(channel='town-square', 236 | message='test message', 237 | url='http://chat.net/hooks/abdegh12') 238 | 239 | @mock.patch('requests.post', side_effect=MockResponse) 240 | def test_send(self, mock_post): 241 | with self.assertRaises(RuntimeError): 242 | payload = mattersend.send(channel='town-square', 243 | message='test message', 244 | url='http://chat.net/hooks/fail') 245 | 246 | def test_attachment(self): 247 | message = mattersend.Message() 248 | message.text = 'test_message' 249 | 250 | attachment = mattersend.Attachment('test_attachment') 251 | message.attachments.append(attachment) 252 | payload = message.get_payload() 253 | self.assertEqual(normalize_payload(payload), r"""{ 254 | "attachments": [ 255 | { 256 | "fallback": "test_attachment", 257 | "text": "test_attachment" 258 | } 259 | ], 260 | "text": "test_message" 261 | }""") 262 | -------------------------------------------------------------------------------- /mattersend.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | import sys 5 | import os 6 | import argparse 7 | import json 8 | import csv 9 | import mimetypes 10 | 11 | from io import StringIO 12 | 13 | try: 14 | import configparser 15 | except ImportError: 16 | import ConfigParser as configparser 17 | 18 | name = 'mattersend' 19 | version = '2.0' 20 | url = 'https://github.com/mtorromeo/mattersend' 21 | description = "Library and CLI utility to send messages to mattermost's incoming webhooks" 22 | 23 | syntaxes = ['diff', 'apache', 'makefile', 'http', 'json', 'markdown', 24 | 'javascript', 'css', 'nginx', 'objectivec', 'python', 'xml', 25 | 'perl', 'bash', 'php', 'coffeescript', 'cs', 'cpp', 'sql', 'go', 26 | 'ruby', 'java', 'ini', 'latex', 'plain', 'auto'] 27 | 28 | mime_to_syntax = { 29 | 'text/x-diff': 'diff', 30 | 'application/json': 'json', 31 | 'application/x-javascript': 'javascript', 32 | 'text/x-python': 'python', 33 | 'application/xml': 'xml', 34 | 'text/x-perl': 'perl', 35 | 'text/x-sh': 'bash', 36 | 'text/x-csrc': 'cpp', 37 | 'text/x-chdr': 'cpp', 38 | 'text/x-c++src': 'cpp', 39 | 'text/x-c++hdr': 'cpp', 40 | 'text/x-c': 'cpp', 41 | 'application/x-sql': 'sql', 42 | 'application/x-ruby': 'ruby', 43 | 'text/x-java-source': 'java', 44 | 'application/x-latex': 'latex', 45 | } 46 | 47 | ext_to_syntax = { 48 | 'Makefile': 'makefile', 49 | '.mk': 'makefile', 50 | '.htaccess': 'apache', 51 | '.json': 'json', 52 | '.js': 'javascript', 53 | '.css': 'css', 54 | '.m': 'objectivec', 55 | '.py': 'python', 56 | '.xml': 'xml', 57 | '.pl': 'perl', 58 | '.sh': 'bash', 59 | '.php': 'php', 60 | '.phtml': 'php', 61 | '.phps': 'php', 62 | '.php3': 'php', 63 | '.php4': 'php', 64 | '.php5': 'php', 65 | '.php7': 'php', 66 | '.coffee': 'coffeescript', 67 | '.cs': 'cs', 68 | '.c': 'cpp', 69 | '.cc': 'cpp', 70 | '.cxx': 'cpp', 71 | '.cpp': 'cpp', 72 | '.h': 'cpp', 73 | '.hh': 'cpp', 74 | '.dic': 'cpp', 75 | '.sql': 'sql', 76 | '.go': 'go', 77 | '.rb': 'ruby', 78 | '.java': 'java', 79 | '.ini': 'ini', 80 | '.latex': 'latex', 81 | } 82 | 83 | emoji_to_code = { 84 | 'smile': '1f604', 85 | 'smiley': '1f603', 86 | 'grinning': '1f600', 87 | 'blush': '1f60a', 88 | 'relaxed': '263a', 89 | 'wink': '1f609', 90 | 'heart_eyes': '1f60d', 91 | 'kissing_heart': '1f618', 92 | 'kissing_closed_eyes': '1f61a', 93 | 'kissing': '1f617', 94 | 'kissing_smiling_eyes': '1f619', 95 | 'stuck_out_tongue_winking_eye': '1f61c', 96 | 'stuck_out_tongue_closed_eyes': '1f61d', 97 | 'stuck_out_tongue': '1f61b', 98 | 'flushed': '1f633', 99 | 'grin': '1f601', 100 | 'pensive': '1f614', 101 | 'relieved': '1f60c', 102 | 'unamused': '1f612', 103 | 'disappointed': '1f61e', 104 | 'persevere': '1f623', 105 | 'cry': '1f622', 106 | 'joy': '1f602', 107 | 'sob': '1f62d', 108 | 'sleepy': '1f62a', 109 | 'disappointed_relieved': '1f625', 110 | 'cold_sweat': '1f630', 111 | 'sweat_smile': '1f605', 112 | 'sweat': '1f613', 113 | 'weary': '1f629', 114 | 'tired_face': '1f62b', 115 | 'fearful': '1f628', 116 | 'scream': '1f631', 117 | 'angry': '1f620', 118 | 'rage': '1f621', 119 | 'pout': '1f621', 120 | 'triumph': '1f624', 121 | 'confounded': '1f616', 122 | 'laughing': '1f606', 123 | 'satisfied': '1f606', 124 | 'yum': '1f60b', 125 | 'mask': '1f637', 126 | 'sunglasses': '1f60e', 127 | 'sleeping': '1f634', 128 | 'dizzy_face': '1f635', 129 | 'astonished': '1f632', 130 | 'worried': '1f61f', 131 | 'frowning': '1f626', 132 | 'anguished': '1f627', 133 | 'smiling_imp': '1f608', 134 | 'imp': '1f47f', 135 | 'open_mouth': '1f62e', 136 | 'grimacing': '1f62c', 137 | 'neutral_face': '1f610', 138 | 'confused': '1f615', 139 | 'hushed': '1f62f', 140 | 'no_mouth': '1f636', 141 | 'innocent': '1f607', 142 | 'smirk': '1f60f', 143 | 'expressionless': '1f611', 144 | 'man_with_gua_pi_mao': '1f472', 145 | 'man_with_turban': '1f473', 146 | 'cop': '1f46e', 147 | 'construction_worker': '1f477', 148 | 'guardsman': '1f482', 149 | 'baby': '1f476', 150 | 'boy': '1f466', 151 | 'girl': '1f467', 152 | 'man': '1f468', 153 | 'woman': '1f469', 154 | 'older_man': '1f474', 155 | 'older_woman': '1f475', 156 | 'person_with_blond_hair': '1f471', 157 | 'angel': '1f47c', 158 | 'princess': '1f478', 159 | 'smiley_cat': '1f63a', 160 | 'smile_cat': '1f638', 161 | 'heart_eyes_cat': '1f63b', 162 | 'kissing_cat': '1f63d', 163 | 'smirk_cat': '1f63c', 164 | 'scream_cat': '1f640', 165 | 'crying_cat_face': '1f63f', 166 | 'joy_cat': '1f639', 167 | 'pouting_cat': '1f63e', 168 | 'japanese_ogre': '1f479', 169 | 'japanese_goblin': '1f47a', 170 | 'see_no_evil': '1f648', 171 | 'hear_no_evil': '1f649', 172 | 'speak_no_evil': '1f64a', 173 | 'skull': '1f480', 174 | 'alien': '1f47d', 175 | 'hankey': '1f4a9', 176 | 'poop': '1f4a9', 177 | 'shit': '1f4a9', 178 | 'fire': '1f525', 179 | 'sparkles': '2728', 180 | 'star2': '1f31f', 181 | 'dizzy': '1f4ab', 182 | 'boom': '1f4a5', 183 | 'collision': '1f4a5', 184 | 'anger': '1f4a2', 185 | 'sweat_drops': '1f4a6', 186 | 'droplet': '1f4a7', 187 | 'zzz': '1f4a4', 188 | 'dash': '1f4a8', 189 | 'ear': '1f442', 190 | 'eyes': '1f440', 191 | 'nose': '1f443', 192 | 'tongue': '1f445', 193 | 'lips': '1f444', 194 | '+1': '1f44d', 195 | 'thumbsup': '1f44d', 196 | '-1': '1f44e', 197 | 'thumbsdown': '1f44e', 198 | 'ok_hand': '1f44c', 199 | 'facepunch': '1f44a', 200 | 'punch': '1f44a', 201 | 'fist': '270a', 202 | 'v': '270c', 203 | 'wave': '1f44b', 204 | 'hand': '270b', 205 | 'raised_hand': '270b', 206 | 'open_hands': '1f450', 207 | 'point_up_2': '1f446', 208 | 'point_down': '1f447', 209 | 'point_right': '1f449', 210 | 'point_left': '1f448', 211 | 'raised_hands': '1f64c', 212 | 'pray': '1f64f', 213 | 'point_up': '261d', 214 | 'clap': '1f44f', 215 | 'muscle': '1f4aa', 216 | 'walking': '1f6b6', 217 | 'runner': '1f3c3', 218 | 'running': '1f3c3', 219 | 'dancer': '1f483', 220 | 'couple': '1f46b', 221 | 'family': '1f46a', 222 | 'two_men_holding_hands': '1f46c', 223 | 'two_women_holding_hands': '1f46d', 224 | 'couplekiss': '1f48f', 225 | 'couple_with_heart': '1f491', 226 | 'dancers': '1f46f', 227 | 'ok_woman': '1f646', 228 | 'no_good': '1f645', 229 | 'ng_woman': '1f645', 230 | 'information_desk_person': '1f481', 231 | 'raising_hand': '1f64b', 232 | 'massage': '1f486', 233 | 'haircut': '1f487', 234 | 'nail_care': '1f485', 235 | 'bride_with_veil': '1f470', 236 | 'person_with_pouting_face': '1f64e', 237 | 'person_frowning': '1f64d', 238 | 'bow': '1f647', 239 | 'tophat': '1f3a9', 240 | 'crown': '1f451', 241 | 'womans_hat': '1f452', 242 | 'athletic_shoe': '1f45f', 243 | 'mans_shoe': '1f45e', 244 | 'shoe': '1f45e', 245 | 'sandal': '1f461', 246 | 'high_heel': '1f460', 247 | 'boot': '1f462', 248 | 'shirt': '1f455', 249 | 'tshirt': '1f455', 250 | 'necktie': '1f454', 251 | 'womans_clothes': '1f45a', 252 | 'dress': '1f457', 253 | 'running_shirt_with_sash': '1f3bd', 254 | 'jeans': '1f456', 255 | 'kimono': '1f458', 256 | 'bikini': '1f459', 257 | 'briefcase': '1f4bc', 258 | 'handbag': '1f45c', 259 | 'pouch': '1f45d', 260 | 'purse': '1f45b', 261 | 'eyeglasses': '1f453', 262 | 'ribbon': '1f380', 263 | 'closed_umbrella': '1f302', 264 | 'lipstick': '1f484', 265 | 'yellow_heart': '1f49b', 266 | 'blue_heart': '1f499', 267 | 'purple_heart': '1f49c', 268 | 'green_heart': '1f49a', 269 | 'heart': '2764', 270 | 'broken_heart': '1f494', 271 | 'heartpulse': '1f497', 272 | 'heartbeat': '1f493', 273 | 'two_hearts': '1f495', 274 | 'sparkling_heart': '1f496', 275 | 'revolving_hearts': '1f49e', 276 | 'cupid': '1f498', 277 | 'love_letter': '1f48c', 278 | 'kiss': '1f48b', 279 | 'ring': '1f48d', 280 | 'gem': '1f48e', 281 | 'bust_in_silhouette': '1f464', 282 | 'busts_in_silhouette': '1f465', 283 | 'speech_balloon': '1f4ac', 284 | 'footprints': '1f463', 285 | 'thought_balloon': '1f4ad', 286 | 'dog': '1f436', 287 | 'wolf': '1f43a', 288 | 'cat': '1f431', 289 | 'mouse': '1f42d', 290 | 'hamster': '1f439', 291 | 'rabbit': '1f430', 292 | 'frog': '1f438', 293 | 'tiger': '1f42f', 294 | 'koala': '1f428', 295 | 'bear': '1f43b', 296 | 'pig': '1f437', 297 | 'pig_nose': '1f43d', 298 | 'cow': '1f42e', 299 | 'boar': '1f417', 300 | 'monkey_face': '1f435', 301 | 'monkey': '1f412', 302 | 'horse': '1f434', 303 | 'sheep': '1f411', 304 | 'elephant': '1f418', 305 | 'panda_face': '1f43c', 306 | 'penguin': '1f427', 307 | 'bird': '1f426', 308 | 'baby_chick': '1f424', 309 | 'hatched_chick': '1f425', 310 | 'hatching_chick': '1f423', 311 | 'chicken': '1f414', 312 | 'snake': '1f40d', 313 | 'turtle': '1f422', 314 | 'bug': '1f41b', 315 | 'bee': '1f41d', 316 | 'honeybee': '1f41d', 317 | 'ant': '1f41c', 318 | 'beetle': '1f41e', 319 | 'snail': '1f40c', 320 | 'octopus': '1f419', 321 | 'shell': '1f41a', 322 | 'tropical_fish': '1f420', 323 | 'fish': '1f41f', 324 | 'dolphin': '1f42c', 325 | 'flipper': '1f42c', 326 | 'whale': '1f433', 327 | 'whale2': '1f40b', 328 | 'cow2': '1f404', 329 | 'ram': '1f40f', 330 | 'rat': '1f400', 331 | 'water_buffalo': '1f403', 332 | 'tiger2': '1f405', 333 | 'rabbit2': '1f407', 334 | 'dragon': '1f409', 335 | 'racehorse': '1f40e', 336 | 'goat': '1f410', 337 | 'rooster': '1f413', 338 | 'dog2': '1f415', 339 | 'pig2': '1f416', 340 | 'mouse2': '1f401', 341 | 'ox': '1f402', 342 | 'dragon_face': '1f432', 343 | 'blowfish': '1f421', 344 | 'crocodile': '1f40a', 345 | 'camel': '1f42b', 346 | 'dromedary_camel': '1f42a', 347 | 'leopard': '1f406', 348 | 'cat2': '1f408', 349 | 'poodle': '1f429', 350 | 'feet': '1f43e', 351 | 'paw_prints': '1f43e', 352 | 'bouquet': '1f490', 353 | 'cherry_blossom': '1f338', 354 | 'tulip': '1f337', 355 | 'four_leaf_clover': '1f340', 356 | 'rose': '1f339', 357 | 'sunflower': '1f33b', 358 | 'hibiscus': '1f33a', 359 | 'maple_leaf': '1f341', 360 | 'leaves': '1f343', 361 | 'fallen_leaf': '1f342', 362 | 'herb': '1f33f', 363 | 'ear_of_rice': '1f33e', 364 | 'mushroom': '1f344', 365 | 'cactus': '1f335', 366 | 'palm_tree': '1f334', 367 | 'evergreen_tree': '1f332', 368 | 'deciduous_tree': '1f333', 369 | 'chestnut': '1f330', 370 | 'seedling': '1f331', 371 | 'blossom': '1f33c', 372 | 'globe_with_meridians': '1f310', 373 | 'sun_with_face': '1f31e', 374 | 'full_moon_with_face': '1f31d', 375 | 'new_moon_with_face': '1f31a', 376 | 'new_moon': '1f311', 377 | 'waxing_crescent_moon': '1f312', 378 | 'first_quarter_moon': '1f313', 379 | 'moon': '1f314', 380 | 'waxing_gibbous_moon': '1f314', 381 | 'full_moon': '1f315', 382 | 'waning_gibbous_moon': '1f316', 383 | 'last_quarter_moon': '1f317', 384 | 'waning_crescent_moon': '1f318', 385 | 'last_quarter_moon_with_face': '1f31c', 386 | 'first_quarter_moon_with_face': '1f31b', 387 | 'crescent_moon': '1f319', 388 | 'earth_africa': '1f30d', 389 | 'earth_americas': '1f30e', 390 | 'earth_asia': '1f30f', 391 | 'volcano': '1f30b', 392 | 'milky_way': '1f30c', 393 | 'stars': '1f320', 394 | 'star': '2b50', 395 | 'sunny': '2600', 396 | 'partly_sunny': '26c5', 397 | 'cloud': '2601', 398 | 'zap': '26a1', 399 | 'umbrella': '2614', 400 | 'snowflake': '2744', 401 | 'snowman': '26c4', 402 | 'cyclone': '1f300', 403 | 'foggy': '1f301', 404 | 'rainbow': '1f308', 405 | 'ocean': '1f30a', 406 | 'bamboo': '1f38d', 407 | 'gift_heart': '1f49d', 408 | 'dolls': '1f38e', 409 | 'school_satchel': '1f392', 410 | 'mortar_board': '1f393', 411 | 'flags': '1f38f', 412 | 'fireworks': '1f386', 413 | 'sparkler': '1f387', 414 | 'wind_chime': '1f390', 415 | 'rice_scene': '1f391', 416 | 'jack_o_lantern': '1f383', 417 | 'ghost': '1f47b', 418 | 'santa': '1f385', 419 | 'christmas_tree': '1f384', 420 | 'gift': '1f381', 421 | 'tanabata_tree': '1f38b', 422 | 'tada': '1f389', 423 | 'confetti_ball': '1f38a', 424 | 'balloon': '1f388', 425 | 'crossed_flags': '1f38c', 426 | 'crystal_ball': '1f52e', 427 | 'movie_camera': '1f3a5', 428 | 'camera': '1f4f7', 429 | 'video_camera': '1f4f9', 430 | 'vhs': '1f4fc', 431 | 'cd': '1f4bf', 432 | 'dvd': '1f4c0', 433 | 'minidisc': '1f4bd', 434 | 'floppy_disk': '1f4be', 435 | 'computer': '1f4bb', 436 | 'iphone': '1f4f1', 437 | 'phone': '260e', 438 | 'telephone': '260e', 439 | 'telephone_receiver': '1f4de', 440 | 'pager': '1f4df', 441 | 'fax': '1f4e0', 442 | 'satellite': '1f4e1', 443 | 'tv': '1f4fa', 444 | 'radio': '1f4fb', 445 | 'loud_sound': '1f50a', 446 | 'sound': '1f509', 447 | 'speaker': '1f508', 448 | 'mute': '1f507', 449 | 'bell': '1f514', 450 | 'no_bell': '1f515', 451 | 'loudspeaker': '1f4e2', 452 | 'mega': '1f4e3', 453 | 'hourglass_flowing_sand': '23f3', 454 | 'hourglass': '231b', 455 | 'alarm_clock': '23f0', 456 | 'watch': '231a', 457 | 'unlock': '1f513', 458 | 'lock': '1f512', 459 | 'lock_with_ink_pen': '1f50f', 460 | 'closed_lock_with_key': '1f510', 461 | 'key': '1f511', 462 | 'mag_right': '1f50e', 463 | 'bulb': '1f4a1', 464 | 'flashlight': '1f526', 465 | 'high_brightness': '1f506', 466 | 'low_brightness': '1f505', 467 | 'electric_plug': '1f50c', 468 | 'battery': '1f50b', 469 | 'mag': '1f50d', 470 | 'bathtub': '1f6c1', 471 | 'bath': '1f6c0', 472 | 'shower': '1f6bf', 473 | 'toilet': '1f6bd', 474 | 'wrench': '1f527', 475 | 'nut_and_bolt': '1f529', 476 | 'hammer': '1f528', 477 | 'door': '1f6aa', 478 | 'smoking': '1f6ac', 479 | 'bomb': '1f4a3', 480 | 'gun': '1f52b', 481 | 'hocho': '1f52a', 482 | 'knife': '1f52a', 483 | 'pill': '1f48a', 484 | 'syringe': '1f489', 485 | 'moneybag': '1f4b0', 486 | 'yen': '1f4b4', 487 | 'dollar': '1f4b5', 488 | 'pound': '1f4b7', 489 | 'euro': '1f4b6', 490 | 'credit_card': '1f4b3', 491 | 'money_with_wings': '1f4b8', 492 | 'calling': '1f4f2', 493 | 'e-mail': '1f4e7', 494 | 'inbox_tray': '1f4e5', 495 | 'outbox_tray': '1f4e4', 496 | 'email': '2709', 497 | 'envelope': '2709', 498 | 'envelope_with_arrow': '1f4e9', 499 | 'incoming_envelope': '1f4e8', 500 | 'postal_horn': '1f4ef', 501 | 'mailbox': '1f4eb', 502 | 'mailbox_closed': '1f4ea', 503 | 'mailbox_with_mail': '1f4ec', 504 | 'mailbox_with_no_mail': '1f4ed', 505 | 'postbox': '1f4ee', 506 | 'package': '1f4e6', 507 | 'memo': '1f4dd', 508 | 'pencil': '1f4dd', 509 | 'page_facing_up': '1f4c4', 510 | 'page_with_curl': '1f4c3', 511 | 'bookmark_tabs': '1f4d1', 512 | 'bar_chart': '1f4ca', 513 | 'chart_with_upwards_trend': '1f4c8', 514 | 'chart_with_downwards_trend': '1f4c9', 515 | 'scroll': '1f4dc', 516 | 'clipboard': '1f4cb', 517 | 'date': '1f4c5', 518 | 'calendar': '1f4c6', 519 | 'card_index': '1f4c7', 520 | 'file_folder': '1f4c1', 521 | 'open_file_folder': '1f4c2', 522 | 'scissors': '2702', 523 | 'pushpin': '1f4cc', 524 | 'paperclip': '1f4ce', 525 | 'black_nib': '2712', 526 | 'pencil2': '270f', 527 | 'straight_ruler': '1f4cf', 528 | 'triangular_ruler': '1f4d0', 529 | 'closed_book': '1f4d5', 530 | 'green_book': '1f4d7', 531 | 'blue_book': '1f4d8', 532 | 'orange_book': '1f4d9', 533 | 'notebook': '1f4d3', 534 | 'notebook_with_decorative_cover': '1f4d4', 535 | 'ledger': '1f4d2', 536 | 'books': '1f4da', 537 | 'book': '1f4d6', 538 | 'open_book': '1f4d6', 539 | 'bookmark': '1f516', 540 | 'name_badge': '1f4db', 541 | 'microscope': '1f52c', 542 | 'telescope': '1f52d', 543 | 'newspaper': '1f4f0', 544 | 'art': '1f3a8', 545 | 'clapper': '1f3ac', 546 | 'microphone': '1f3a4', 547 | 'headphones': '1f3a7', 548 | 'musical_score': '1f3bc', 549 | 'musical_note': '1f3b5', 550 | 'notes': '1f3b6', 551 | 'musical_keyboard': '1f3b9', 552 | 'violin': '1f3bb', 553 | 'trumpet': '1f3ba', 554 | 'saxophone': '1f3b7', 555 | 'guitar': '1f3b8', 556 | 'space_invader': '1f47e', 557 | 'video_game': '1f3ae', 558 | 'black_joker': '1f0cf', 559 | 'flower_playing_cards': '1f3b4', 560 | 'mahjong': '1f004', 561 | 'game_die': '1f3b2', 562 | 'dart': '1f3af', 563 | 'football': '1f3c8', 564 | 'basketball': '1f3c0', 565 | 'soccer': '26bd', 566 | 'baseball': '26be', 567 | 'tennis': '1f3be', 568 | '8ball': '1f3b1', 569 | 'rugby_football': '1f3c9', 570 | 'bowling': '1f3b3', 571 | 'golf': '26f3', 572 | 'mountain_bicyclist': '1f6b5', 573 | 'bicyclist': '1f6b4', 574 | 'checkered_flag': '1f3c1', 575 | 'horse_racing': '1f3c7', 576 | 'trophy': '1f3c6', 577 | 'ski': '1f3bf', 578 | 'snowboarder': '1f3c2', 579 | 'swimmer': '1f3ca', 580 | 'surfer': '1f3c4', 581 | 'fishing_pole_and_fish': '1f3a3', 582 | 'coffee': '2615', 583 | 'tea': '1f375', 584 | 'sake': '1f376', 585 | 'baby_bottle': '1f37c', 586 | 'beer': '1f37a', 587 | 'beers': '1f37b', 588 | 'cocktail': '1f378', 589 | 'tropical_drink': '1f379', 590 | 'wine_glass': '1f377', 591 | 'fork_and_knife': '1f374', 592 | 'pizza': '1f355', 593 | 'hamburger': '1f354', 594 | 'fries': '1f35f', 595 | 'poultry_leg': '1f357', 596 | 'meat_on_bone': '1f356', 597 | 'spaghetti': '1f35d', 598 | 'curry': '1f35b', 599 | 'fried_shrimp': '1f364', 600 | 'bento': '1f371', 601 | 'sushi': '1f363', 602 | 'fish_cake': '1f365', 603 | 'rice_ball': '1f359', 604 | 'rice_cracker': '1f358', 605 | 'rice': '1f35a', 606 | 'ramen': '1f35c', 607 | 'stew': '1f372', 608 | 'oden': '1f362', 609 | 'dango': '1f361', 610 | 'egg': '1f373', 611 | 'bread': '1f35e', 612 | 'doughnut': '1f369', 613 | 'custard': '1f36e', 614 | 'icecream': '1f366', 615 | 'ice_cream': '1f368', 616 | 'shaved_ice': '1f367', 617 | 'birthday': '1f382', 618 | 'cake': '1f370', 619 | 'cookie': '1f36a', 620 | 'chocolate_bar': '1f36b', 621 | 'candy': '1f36c', 622 | 'lollipop': '1f36d', 623 | 'honey_pot': '1f36f', 624 | 'apple': '1f34e', 625 | 'green_apple': '1f34f', 626 | 'tangerine': '1f34a', 627 | 'orange': '1f34a', 628 | 'mandarin': '1f34a', 629 | 'lemon': '1f34b', 630 | 'cherries': '1f352', 631 | 'grapes': '1f347', 632 | 'watermelon': '1f349', 633 | 'strawberry': '1f353', 634 | 'peach': '1f351', 635 | 'melon': '1f348', 636 | 'banana': '1f34c', 637 | 'pear': '1f350', 638 | 'pineapple': '1f34d', 639 | 'sweet_potato': '1f360', 640 | 'eggplant': '1f346', 641 | 'tomato': '1f345', 642 | 'corn': '1f33d', 643 | 'house': '1f3e0', 644 | 'house_with_garden': '1f3e1', 645 | 'school': '1f3eb', 646 | 'office': '1f3e2', 647 | 'post_office': '1f3e3', 648 | 'hospital': '1f3e5', 649 | 'bank': '1f3e6', 650 | 'convenience_store': '1f3ea', 651 | 'love_hotel': '1f3e9', 652 | 'hotel': '1f3e8', 653 | 'wedding': '1f492', 654 | 'church': '26ea', 655 | 'department_store': '1f3ec', 656 | 'european_post_office': '1f3e4', 657 | 'city_sunrise': '1f307', 658 | 'city_sunset': '1f306', 659 | 'japanese_castle': '1f3ef', 660 | 'european_castle': '1f3f0', 661 | 'tent': '26fa', 662 | 'factory': '1f3ed', 663 | 'tokyo_tower': '1f5fc', 664 | 'japan': '1f5fe', 665 | 'mount_fuji': '1f5fb', 666 | 'sunrise_over_mountains': '1f304', 667 | 'sunrise': '1f305', 668 | 'night_with_stars': '1f303', 669 | 'statue_of_liberty': '1f5fd', 670 | 'bridge_at_night': '1f309', 671 | 'carousel_horse': '1f3a0', 672 | 'ferris_wheel': '1f3a1', 673 | 'fountain': '26f2', 674 | 'roller_coaster': '1f3a2', 675 | 'ship': '1f6a2', 676 | 'boat': '26f5', 677 | 'sailboat': '26f5', 678 | 'speedboat': '1f6a4', 679 | 'rowboat': '1f6a3', 680 | 'anchor': '2693', 681 | 'rocket': '1f680', 682 | 'airplane': '2708', 683 | 'seat': '1f4ba', 684 | 'helicopter': '1f681', 685 | 'steam_locomotive': '1f682', 686 | 'tram': '1f68a', 687 | 'station': '1f689', 688 | 'mountain_railway': '1f69e', 689 | 'train2': '1f686', 690 | 'bullettrain_side': '1f684', 691 | 'bullettrain_front': '1f685', 692 | 'light_rail': '1f688', 693 | 'metro': '1f687', 694 | 'monorail': '1f69d', 695 | 'train': '1f68b', 696 | 'railway_car': '1f683', 697 | 'trolleybus': '1f68e', 698 | 'bus': '1f68c', 699 | 'oncoming_bus': '1f68d', 700 | 'blue_car': '1f699', 701 | 'oncoming_automobile': '1f698', 702 | 'car': '1f697', 703 | 'red_car': '1f697', 704 | 'taxi': '1f695', 705 | 'oncoming_taxi': '1f696', 706 | 'articulated_lorry': '1f69b', 707 | 'truck': '1f69a', 708 | 'rotating_light': '1f6a8', 709 | 'police_car': '1f693', 710 | 'oncoming_police_car': '1f694', 711 | 'fire_engine': '1f692', 712 | 'ambulance': '1f691', 713 | 'minibus': '1f690', 714 | 'bike': '1f6b2', 715 | 'aerial_tramway': '1f6a1', 716 | 'suspension_railway': '1f69f', 717 | 'mountain_cableway': '1f6a0', 718 | 'tractor': '1f69c', 719 | 'barber': '1f488', 720 | 'busstop': '1f68f', 721 | 'ticket': '1f3ab', 722 | 'vertical_traffic_light': '1f6a6', 723 | 'traffic_light': '1f6a5', 724 | 'warning': '26a0', 725 | 'construction': '1f6a7', 726 | 'beginner': '1f530', 727 | 'fuelpump': '26fd', 728 | 'izakaya_lantern': '1f3ee', 729 | 'lantern': '1f3ee', 730 | 'slot_machine': '1f3b0', 731 | 'hotsprings': '2668', 732 | 'moyai': '1f5ff', 733 | 'circus_tent': '1f3aa', 734 | 'performing_arts': '1f3ad', 735 | 'round_pushpin': '1f4cd', 736 | 'triangular_flag_on_post': '1f6a9', 737 | 'jp': '1f1ef-1f1f5', 738 | 'kr': '1f1f0-1f1f7', 739 | 'de': '1f1e9-1f1ea', 740 | 'cn': '1f1e8-1f1f3', 741 | 'us': '1f1fa-1f1f8', 742 | 'fr': '1f1eb-1f1f7', 743 | 'es': '1f1ea-1f1f8', 744 | 'it': '1f1ee-1f1f9', 745 | 'ru': '1f1f7-1f1fa', 746 | 'gb': '1f1ec-1f1e7', 747 | 'uk': '1f1ec-1f1e7', 748 | 'one': '0031-20e3', 749 | 'two': '0032-20e3', 750 | 'three': '0033-20e3', 751 | 'four': '0034-20e3', 752 | 'five': '0035-20e3', 753 | 'six': '0036-20e3', 754 | 'seven': '0037-20e3', 755 | 'eight': '0038-20e3', 756 | 'nine': '0039-20e3', 757 | 'zero': '0030-20e3', 758 | 'keycap_ten': '1f51f', 759 | '1234': '1f522', 760 | 'hash': '0023-20e3', 761 | 'symbols': '1f523', 762 | 'arrow_up': '2b06', 763 | 'arrow_down': '2b07', 764 | 'arrow_left': '2b05', 765 | 'arrow_right': '27a1', 766 | 'capital_abcd': '1f520', 767 | 'abcd': '1f521', 768 | 'abc': '1f524', 769 | 'arrow_upper_right': '2197', 770 | 'arrow_upper_left': '2196', 771 | 'arrow_lower_right': '2198', 772 | 'arrow_lower_left': '2199', 773 | 'left_right_arrow': '2194', 774 | 'arrow_up_down': '2195', 775 | 'arrows_counterclockwise': '1f504', 776 | 'arrow_backward': '25c0', 777 | 'arrow_forward': '25b6', 778 | 'arrow_up_small': '1f53c', 779 | 'arrow_down_small': '1f53d', 780 | 'leftwards_arrow_with_hook': '21a9', 781 | 'arrow_right_hook': '21aa', 782 | 'information_source': '2139', 783 | 'rewind': '23ea', 784 | 'fast_forward': '23e9', 785 | 'arrow_double_up': '23eb', 786 | 'arrow_double_down': '23ec', 787 | 'arrow_heading_down': '2935', 788 | 'arrow_heading_up': '2934', 789 | 'ok': '1f197', 790 | 'twisted_rightwards_arrows': '1f500', 791 | 'repeat': '1f501', 792 | 'repeat_one': '1f502', 793 | 'new': '1f195', 794 | 'up': '1f199', 795 | 'cool': '1f192', 796 | 'free': '1f193', 797 | 'ng': '1f196', 798 | 'signal_strength': '1f4f6', 799 | 'cinema': '1f3a6', 800 | 'koko': '1f201', 801 | 'u6307': '1f22f', 802 | 'u7a7a': '1f233', 803 | 'u6e80': '1f235', 804 | 'u5408': '1f234', 805 | 'u7981': '1f232', 806 | 'ideograph_advantage': '1f250', 807 | 'u5272': '1f239', 808 | 'u55b6': '1f23a', 809 | 'u6709': '1f236', 810 | 'u7121': '1f21a', 811 | 'restroom': '1f6bb', 812 | 'mens': '1f6b9', 813 | 'womens': '1f6ba', 814 | 'baby_symbol': '1f6bc', 815 | 'wc': '1f6be', 816 | 'potable_water': '1f6b0', 817 | 'put_litter_in_its_place': '1f6ae', 818 | 'parking': '1f17f', 819 | 'wheelchair': '267f', 820 | 'no_smoking': '1f6ad', 821 | 'u6708': '1f237', 822 | 'u7533': '1f238', 823 | 'sa': '1f202', 824 | 'm': '24c2', 825 | 'passport_control': '1f6c2', 826 | 'baggage_claim': '1f6c4', 827 | 'left_luggage': '1f6c5', 828 | 'customs': '1f6c3', 829 | 'accept': '1f251', 830 | 'secret': '3299', 831 | 'congratulations': '3297', 832 | 'cl': '1f191', 833 | 'sos': '1f198', 834 | 'id': '1f194', 835 | 'no_entry_sign': '1f6ab', 836 | 'underage': '1f51e', 837 | 'no_mobile_phones': '1f4f5', 838 | 'do_not_litter': '1f6af', 839 | 'non-potable_water': '1f6b1', 840 | 'no_bicycles': '1f6b3', 841 | 'no_pedestrians': '1f6b7', 842 | 'children_crossing': '1f6b8', 843 | 'no_entry': '26d4', 844 | 'eight_spoked_asterisk': '2733', 845 | 'sparkle': '2747', 846 | 'negative_squared_cross_mark': '274e', 847 | 'white_check_mark': '2705', 848 | 'eight_pointed_black_star': '2734', 849 | 'heart_decoration': '1f49f', 850 | 'vs': '1f19a', 851 | 'vibration_mode': '1f4f3', 852 | 'mobile_phone_off': '1f4f4', 853 | 'a': '1f170', 854 | 'b': '1f171', 855 | 'ab': '1f18e', 856 | 'o2': '1f17e', 857 | 'diamond_shape_with_a_dot_inside': '1f4a0', 858 | 'loop': '27bf', 859 | 'recycle': '267b', 860 | 'aries': '2648', 861 | 'taurus': '2649', 862 | 'gemini': '264a', 863 | 'cancer': '264b', 864 | 'leo': '264c', 865 | 'virgo': '264d', 866 | 'libra': '264e', 867 | 'scorpius': '264f', 868 | 'sagittarius': '2650', 869 | 'capricorn': '2651', 870 | 'aquarius': '2652', 871 | 'pisces': '2653', 872 | 'ophiuchus': '26ce', 873 | 'six_pointed_star': '1f52f', 874 | 'atm': '1f3e7', 875 | 'chart': '1f4b9', 876 | 'heavy_dollar_sign': '1f4b2', 877 | 'currency_exchange': '1f4b1', 878 | 'copyright': '00a9', 879 | 'registered': '00ae', 880 | 'tm': '2122', 881 | 'x': '274c', 882 | 'bangbang': '203c', 883 | 'interrobang': '2049', 884 | 'exclamation': '2757', 885 | 'heavy_exclamation_mark': '2757', 886 | 'question': '2753', 887 | 'grey_exclamation': '2755', 888 | 'grey_question': '2754', 889 | 'o': '2b55', 890 | 'top': '1f51d', 891 | 'end': '1f51a', 892 | 'back': '1f519', 893 | 'on': '1f51b', 894 | 'soon': '1f51c', 895 | 'arrows_clockwise': '1f503', 896 | 'clock12': '1f55b', 897 | 'clock1230': '1f567', 898 | 'clock1': '1f550', 899 | 'clock130': '1f55c', 900 | 'clock2': '1f551', 901 | 'clock230': '1f55d', 902 | 'clock3': '1f552', 903 | 'clock330': '1f55e', 904 | 'clock4': '1f553', 905 | 'clock430': '1f55f', 906 | 'clock5': '1f554', 907 | 'clock530': '1f560', 908 | 'clock6': '1f555', 909 | 'clock7': '1f556', 910 | 'clock8': '1f557', 911 | 'clock9': '1f558', 912 | 'clock10': '1f559', 913 | 'clock11': '1f55a', 914 | 'clock630': '1f561', 915 | 'clock730': '1f562', 916 | 'clock830': '1f563', 917 | 'clock930': '1f564', 918 | 'clock1030': '1f565', 919 | 'clock1130': '1f566', 920 | 'heavy_multiplication_x': '2716', 921 | 'heavy_plus_sign': '2795', 922 | 'heavy_minus_sign': '2796', 923 | 'heavy_division_sign': '2797', 924 | 'spades': '2660', 925 | 'hearts': '2665', 926 | 'clubs': '2663', 927 | 'diamonds': '2666', 928 | 'white_flower': '1f4ae', 929 | '100': '1f4af', 930 | 'heavy_check_mark': '2714', 931 | 'ballot_box_with_check': '2611', 932 | 'radio_button': '1f518', 933 | 'link': '1f517', 934 | 'curly_loop': '27b0', 935 | 'wavy_dash': '3030', 936 | 'part_alternation_mark': '303d', 937 | 'trident': '1f531', 938 | 'black_medium_square': '25fc', 939 | 'white_medium_square': '25fb', 940 | 'black_medium_small_square': '25fe', 941 | 'white_medium_small_square': '25fd', 942 | 'black_small_square': '25aa', 943 | 'white_small_square': '25ab', 944 | 'small_red_triangle': '1f53a', 945 | 'black_square_button': '1f532', 946 | 'white_square_button': '1f533', 947 | 'black_circle': '26ab', 948 | 'white_circle': '26aa', 949 | 'red_circle': '1f534', 950 | 'large_blue_circle': '1f535', 951 | 'small_red_triangle_down': '1f53b', 952 | 'white_large_square': '2b1c', 953 | 'black_large_square': '2b1b', 954 | 'large_orange_diamond': '1f536', 955 | 'large_blue_diamond': '1f537', 956 | 'small_orange_diamond': '1f538', 957 | 'small_blue_diamond': '1f539', 958 | 'ca': '1f1e8-1f1e6', 959 | 'eh': '1f1e8-1f1e6', 960 | 'pk': '1f1f5-1f1f0', 961 | 'za': '1f1ff-1f1e6', 962 | 'slightly_smiling_face': '1f642', 963 | 'slightly_frowning_face': '1f641', 964 | 'upside_down_face': '1f643', 965 | 'mm': 'mm', 966 | 'mattermost': 'mattermost', 967 | 'basecamp': 'basecamp', 968 | 'basecampy': 'basecampy', 969 | 'bowtie': 'bowtie', 970 | 'feelsgood': 'feelsgood', 971 | 'finnadie': 'finnadie', 972 | 'fu': 'fu', 973 | 'goberserk': 'goberserk', 974 | 'godmode': 'godmode', 975 | 'hurtrealbad': 'hurtrealbad', 976 | 'metal': 'metal', 977 | 'neckbeard': 'neckbeard', 978 | 'octocat': 'octocat', 979 | 'rage1': 'rage1', 980 | 'rage2': 'rage2', 981 | 'rage3': 'rage3', 982 | 'rage4': 'rage4', 983 | 'shipit': 'shipit', 984 | 'squirrel': 'squirrel', 985 | 'suspect': 'suspect', 986 | 'taco': 'taco', 987 | 'trollface': 'trollface', 988 | } 989 | 990 | 991 | def detect_syntax(basename, mime): 992 | if mime in mime_to_syntax: 993 | return mime_to_syntax[mime] 994 | 995 | (_, ext) = os.path.splitext(basename) 996 | if not ext: 997 | ext = basename 998 | return ext_to_syntax[ext] if ext in ext_to_syntax else None 999 | 1000 | 1001 | def sizeof_fmt(num, suffix='B'): 1002 | for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']: 1003 | if abs(num) < 1024.0: 1004 | return "%3.1f%s%s" % (num, unit, suffix) 1005 | num /= 1024.0 1006 | return "%.1f%s%s" % (num, 'Yi', suffix) 1007 | 1008 | 1009 | def md_table(data): 1010 | md = [] 1011 | for i, row in enumerate(data): 1012 | if i == 1: 1013 | md.append("| --- " * len(row) + "|") 1014 | md.append("| {} |".format(" | ".join( 1015 | [str(cell).replace("|", "❘").replace("\n", " ").replace("\r", " ") for cell in row] 1016 | ))) 1017 | return "\n".join(md) 1018 | 1019 | 1020 | class Message: 1021 | def __init__(self, channel=None, url=None, username=None, icon=None, 1022 | config_section='DEFAULT', config_name='mattersend', 1023 | config_file=None): 1024 | # CONFIG file 1025 | config = configparser.ConfigParser() 1026 | 1027 | if config_file: 1028 | config.read(config_file) 1029 | elif config_name: 1030 | config.read(["/etc/{}.conf".format(config_name), os.path.expanduser("~/.{}.conf".format(config_name))]) 1031 | 1032 | config = dict(config.items(config_section)) 1033 | 1034 | # merge config file with cli arguments 1035 | self.url = config.get('url') if url is None else url 1036 | self.channel = config.get('channel') if channel is None else channel 1037 | self.username = config.get('username') if username is None else username 1038 | self.icon = config.get('icon') if icon is None else icon 1039 | 1040 | self.text = '' 1041 | self.attachments = [] 1042 | 1043 | def get_payload(self): 1044 | payload = {} 1045 | 1046 | for opt in ('text', 'channel', 'username'): 1047 | optvalue = getattr(self, opt) 1048 | if optvalue is not None: 1049 | payload[opt] = optvalue.strip() 1050 | 1051 | opt, optvalue = self.get_icon() 1052 | if optvalue is not None: 1053 | payload[opt] = optvalue 1054 | 1055 | if self.attachments: 1056 | payload['attachments'] = [a.data() for a in self.attachments] 1057 | 1058 | return json.dumps(payload, sort_keys=True, indent=4) 1059 | 1060 | def get_icon(self): 1061 | if self.icon is None: 1062 | return None, None 1063 | 1064 | ioptvalue = self.icon.strip() 1065 | ioptname = 'icon_url' if '://' in ioptvalue else 'icon_emoji' 1066 | 1067 | # workaround mattermost missing icon_emoji until implemented 1068 | if ioptname == 'icon_emoji' and ioptvalue[0] == ':' and ioptvalue[-1] == ':': 1069 | ioptvalue = emoji_to_code.get(ioptvalue[1:-1], False) 1070 | if ioptvalue: 1071 | baseurl = self.url.split('/hooks/', 1) 1072 | if len(baseurl) == 2: 1073 | ioptname = 'icon_url' 1074 | ioptvalue = '{0}/static/emoji/{1}.png'.format(baseurl[0], ioptvalue) 1075 | 1076 | return ioptname, ioptvalue 1077 | 1078 | def append(self, text, separator=None): 1079 | if self.text and separator is not None: 1080 | self.text += separator 1081 | self.text += text 1082 | 1083 | def attach_file(self, filename, text=None, tabular=False, syntax='auto', fileinfo=False): 1084 | attachment = Attachment() 1085 | 1086 | if tabular: 1087 | syntax = None 1088 | 1089 | (mime, _) = mimetypes.guess_type(filename) 1090 | attachment.title = os.path.basename(filename) 1091 | 1092 | if text is None: 1093 | with open(filename, 'rUb') as f: 1094 | text = f.read().decode('utf-8') 1095 | 1096 | if tabular: 1097 | csvfile = StringIO(text.strip()) 1098 | 1099 | if tabular == 'sniff': 1100 | dialect = csv.Sniffer().sniff(text) 1101 | else: 1102 | dialect = tabular 1103 | 1104 | text = md_table(csv.reader(csvfile, dialect)) 1105 | 1106 | elif syntax == 'auto': 1107 | syntax = detect_syntax(attachment.title, mime) 1108 | 1109 | if syntax is not None: 1110 | text = md_code(text, syntax) 1111 | 1112 | attachment.text = text 1113 | 1114 | if fileinfo: 1115 | statinfo = os.stat(filename) 1116 | attachment.add_field('Size', sizeof_fmt(statinfo.st_size), True) 1117 | attachment.add_field('Mime', mime, True) 1118 | 1119 | self.attachments.append(attachment) 1120 | return attachment 1121 | 1122 | def send(self): 1123 | if self.url is None: 1124 | raise TypeError('Missing mattermost webhook URL') 1125 | 1126 | if self.channel is None: 1127 | raise TypeError('Missing destination channel') 1128 | 1129 | import requests 1130 | 1131 | payload = self.get_payload() 1132 | r = requests.post(self.url, data={'payload': payload}) 1133 | 1134 | if r.status_code != 200: 1135 | try: 1136 | r = json.loads(r.text) 1137 | except ValueError: 1138 | r = {'message': r.text, 'status_code': r.status_code} 1139 | raise RuntimeError("{} ({})".format(r['message'], r['status_code'])) 1140 | 1141 | return r 1142 | 1143 | 1144 | class Attachment: 1145 | def __init__(self, text=''): 1146 | self.text = text 1147 | self.color = None 1148 | self.pretext = None 1149 | self.fallback = None 1150 | 1151 | self.author_name = None 1152 | self.author_link = None 1153 | self.author_icon = None 1154 | 1155 | self.title = None 1156 | self.title_link = None 1157 | 1158 | self.image_url = None 1159 | self.thumb_url = None 1160 | 1161 | self.fields = [] 1162 | 1163 | def set_author(self, name, link=None, icon=None): 1164 | self.author_name = name 1165 | self.author_link = link 1166 | self.author_icon = icon 1167 | 1168 | def set_title(self, title, link=None): 1169 | self.title = title 1170 | self.title_link = link 1171 | 1172 | def add_field(self, title, value, short=None): 1173 | field = { 1174 | 'title': str(title), 1175 | 'value': str(value), 1176 | } 1177 | if short is not None: 1178 | field['short'] = bool(short) 1179 | self.fields.append(field) 1180 | 1181 | def data(self): 1182 | data = {k: v for (k, v) in self.__dict__.items() if v} 1183 | if not self.fallback: 1184 | data['fallback'] = self.text 1185 | # 4000+ chars triggers error on mattermost, not sure where the limit is 1186 | data['text'] = data['text'][:3500] 1187 | data['fallback'] = data['fallback'][:3500] 1188 | return data 1189 | 1190 | 1191 | def md_code(code, syntax='plain'): 1192 | if syntax == 'plain': 1193 | syntax = '' 1194 | return "```{}\n{}```".format(syntax, code) 1195 | 1196 | 1197 | def main(): 1198 | try: 1199 | import setproctitle 1200 | setproctitle.setproctitle(name) 1201 | except ImportError: 1202 | pass 1203 | 1204 | dialects = csv.list_dialects() 1205 | dialects.sort() 1206 | dialects.insert(0, 'sniff') 1207 | 1208 | # CLI arguments 1209 | parser = argparse.ArgumentParser(prog=name, description=description) 1210 | 1211 | parser.add_argument('-V', '--version', action='version', version="%(prog)s " + version) 1212 | parser.add_argument('-C', '--config', help='Use a different configuration file') 1213 | parser.add_argument('-s', '--section', help='Configuration file section', default='DEFAULT') 1214 | parser.add_argument('-c', '--channel', help='Send to this channel or @username') 1215 | parser.add_argument('-U', '--url', help='Mattermost webhook URL') 1216 | parser.add_argument('-u', '--username', help='Username') 1217 | parser.add_argument('-i', '--icon', help='Icon') 1218 | 1219 | group = parser.add_mutually_exclusive_group() 1220 | group.add_argument('-t', '--tabular', metavar='DIALECT', const='sniff', 1221 | nargs='?', choices=dialects, 1222 | help='Parse input as CSV and format it as a table (DIALECT can be one of %(choices)s)') 1223 | group.add_argument('-y', '--syntax', default='auto') 1224 | 1225 | parser.add_argument('-I', '--info', action='store_true', 1226 | help='Include file information in message') 1227 | parser.add_argument('-n', '--dry-run', '--just-print', action='store_true', 1228 | help="Don't send, just print the payload") 1229 | parser.add_argument('-f', '--file', default='-', 1230 | help="Read content from FILE. If - reads from standard input (DEFAULT: %(default)s)") 1231 | 1232 | args = parser.parse_args() 1233 | 1234 | if args.file == '-': 1235 | message = sys.stdin.read() 1236 | filename = None 1237 | else: 1238 | message = '' 1239 | filename = args.file 1240 | 1241 | 1242 | try: 1243 | payload = send(args.channel, message, filename, args.url, 1244 | args.username, args.icon, args.syntax, args.tabular, 1245 | args.info, args.dry_run, args.section, name, 1246 | args.config) 1247 | except (configparser.Error, TypeError, RuntimeError) as e: 1248 | sys.exit(str(e)) 1249 | 1250 | if args.dry_run: 1251 | print(payload) 1252 | 1253 | 1254 | def send(channel, message='', filename=False, url=None, username=None, 1255 | icon=None, syntax='auto', tabular=False, fileinfo=False, 1256 | just_return=False, config_section='DEFAULT', 1257 | config_name='mattersend', config_file=None): 1258 | msg = Message(channel, url, username, icon, config_section, 1259 | config_name, config_file) 1260 | 1261 | if filename: 1262 | if syntax == 'none': 1263 | syntax = None 1264 | msg.attach_file(filename, None, tabular, syntax, fileinfo) 1265 | else: 1266 | if tabular: 1267 | syntax = None 1268 | csvfile = StringIO(message.strip()) 1269 | 1270 | if tabular == 'sniff': 1271 | dialect = csv.Sniffer().sniff(message) 1272 | else: 1273 | dialect = tabular 1274 | 1275 | message = md_table(csv.reader(csvfile, dialect)) 1276 | 1277 | elif syntax in ('auto', 'none'): 1278 | syntax = None 1279 | 1280 | if syntax is not None: 1281 | message = md_code(message, syntax) 1282 | 1283 | msg.text = message 1284 | 1285 | if just_return: 1286 | payload = msg.get_payload() 1287 | return "POST {}\n{}".format(msg.url, payload) 1288 | 1289 | msg.send() 1290 | 1291 | 1292 | if __name__ == '__main__': 1293 | main() 1294 | --------------------------------------------------------------------------------