├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── bot_commands.py ├── callbacks.py ├── chat_functions.py ├── command_dict.py ├── commands.yaml.example ├── config.py ├── config.yaml.example ├── docker-compose.yml ├── eno ├── README.md ├── logos │ ├── Nsibidi-small.png │ ├── Nsibidi.png │ ├── README.md │ ├── eno-logo.png │ └── eno-logo.svg ├── matrix-eno-bot.service.example └── scripts │ ├── README.md │ ├── affirmations.sh │ ├── alert.sh │ ├── backup.sh │ ├── btc.sh │ ├── check.sh │ ├── config.rc.example │ ├── cputemp.sh │ ├── datetime.sh │ ├── ddg.sh │ ├── disks.sh │ ├── echo.py │ ├── eth.sh │ ├── firewall.sh │ ├── hello.sh │ ├── hn.sh │ ├── mn.sh │ ├── motd.sh │ ├── platforminfo.py │ ├── ps.sh │ ├── restart.sh │ ├── rss.sh │ ├── rssread.py │ ├── s2f.sh │ ├── tides.sh │ ├── top.sh │ ├── totp.sh │ ├── twitter.sh │ ├── update.sh │ ├── users.sh │ ├── wake.sh │ ├── waves.sh │ ├── weather.sh │ ├── web.sh │ ├── whoami.py │ └── xmr.sh ├── errors.py ├── main.py ├── message_responses.py ├── requirements.txt ├── room_dict.py └── storage.py /.gitignore: -------------------------------------------------------------------------------- 1 | # PyCharm 2 | .idea/ 3 | 4 | # Python virtualenv environment folders 5 | env/ 6 | env3/ 7 | .env/ 8 | 9 | # Bot local files 10 | *.db 11 | 12 | # Python 13 | __pycache__/ 14 | eno/scripts/__pycache__/ 15 | 16 | # Config file 17 | config.yaml 18 | eno/matrix-eno-bot.service 19 | eno/scripts/config.rc 20 | 21 | # e2ee store 22 | store/ 23 | 24 | # Log files 25 | *.log 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-slim 2 | 3 | RUN apt update && apt upgrade -y && \ 4 | apt install -y \ 5 | wget \ 6 | libmagic1 \ 7 | build-essential 8 | 9 | WORKDIR /bot 10 | 11 | COPY *.py /bot/ 12 | COPY *.yaml /bot/ 13 | COPY *.txt /bot/ 14 | COPY eno /bot/eno/ 15 | 16 | # download libolm3 from Ubuntu focal distribution 17 | # https://packages.ubuntu.com/focal/libolm3 18 | # https://packages.ubuntu.com/focal/libolm-dev 19 | RUN wget http://mirrors.kernel.org/ubuntu/pool/universe/o/olm/libolm-dev_3.1.3+dfsg-2build2_amd64.deb 20 | RUN wget http://mirrors.kernel.org/ubuntu/pool/universe/o/olm/libolm3_3.1.3+dfsg-2build2_amd64.deb 21 | RUN dpkg -i ./*.deb 22 | RUN pip install -r requirements.txt 23 | 24 | # clean up apt cache and remove gcc 25 | RUN apt purge -y build-essential && \ 26 | apt autoremove -y && apt clean && \ 27 | rm -rf /var/lib/apt/lists/* 28 | 29 | CMD [ "python", "./main.py" ] 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /bot_commands.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | r"""bot_commands.py. 4 | 5 | 0123456789012345678901234567890123456789012345678901234567890123456789012345678 6 | 0000000000111111111122222222223333333333444444444455555555556666666666777777777 7 | 8 | # bot_commands.py 9 | 10 | See the implemented sample bot commands of `echo`, `date`, `dir`, `help`, 11 | and `whoami`? Have a close look at them and style your commands after these 12 | example commands. 13 | 14 | Don't change tabbing, spacing, or formating as the 15 | file is automatically linted and beautified. 16 | 17 | """ 18 | 19 | import getpass 20 | import logging 21 | import os 22 | import re # regular expression matching 23 | import subprocess 24 | from sys import platform 25 | import traceback 26 | import time 27 | from chat_functions import send_text_to_room 28 | 29 | logger = logging.getLogger(__name__) 30 | 31 | SERVER_ERROR_MSG = "Bot encountered an error. Here is the stack trace: \n" 32 | 33 | 34 | class Command(object): 35 | """Use this class for your bot commands.""" 36 | 37 | def __init__(self, client, store, config, command_dict, command, room_dict, room, event): 38 | """Set up bot commands. 39 | 40 | Arguments: 41 | --------- 42 | client (nio.AsyncClient): The client to communicate with Matrix 43 | store (Storage): Bot storage 44 | config (Config): Bot configuration parameters 45 | command_dict (CommandDict): Command dictionary 46 | command (str): The command and arguments 47 | room_dict (RoomDict): Room dictionary 48 | room (nio.rooms.MatrixRoom): The room the command was sent in 49 | event (nio.events.room_events.RoomMessageText): The event 50 | describing the command 51 | 52 | """ 53 | self.client = client 54 | self.store = store 55 | self.config = config 56 | self.command_dict = command_dict 57 | self.command = command 58 | self.room_dict = room_dict 59 | self.room = room 60 | self.event = event 61 | # self.args: list : list of arguments 62 | self.args = re.findall(r'(?:[^\s,"]|"(?:\\.|[^"])*")+', self.command)[ 63 | 1: 64 | ] 65 | # will work for double quotes " 66 | # will work for 'a bb ccc "e e"' --> ['a', 'bb', 'ccc', '"e e"'] 67 | # will not work for single quotes ' 68 | # will not work for "a bb ccc 'e e'" --> ['a', 'bb', 'ccc', "'e", "e'"] 69 | self.commandlower = self.command.lower() 70 | 71 | async def process(self): # noqa 72 | """Process the command.""" 73 | 74 | logger.debug( 75 | f"bot_commands :: Command.process: processing '" + 76 | re.sub('^data:(\w+)/(\w+);(.+)', 'data:\\1/\\2;...', self.command) + 77 | f"' from room '{self.room.display_name}'" 78 | ) 79 | 80 | if re.match( 81 | "^help$|^ayuda$|^man$|^manual$|^hilfe$|" 82 | "^je suis perdu$|^perdu$|^socorro$|^h$|" 83 | "^rescate$|^rescate .*|^help .*|^help.sh$", 84 | self.commandlower, 85 | ): 86 | await self._show_help() 87 | 88 | # command from room dict 89 | elif self.room_dict.match(self.room.display_name): 90 | matched_cmd = self.room_dict.get_last_matched_room() 91 | await self._os_cmd( 92 | cmd=self.room_dict.get_cmd(matched_cmd), 93 | args=self.room_dict.get_opt_args(matched_cmd), 94 | markdown_convert=self.room_dict.get_opt_markdown_convert(matched_cmd), 95 | formatted=self.room_dict.get_opt_formatted(matched_cmd), 96 | code=self.room_dict.get_opt_code(matched_cmd), 97 | split=self.room_dict.get_opt_split(matched_cmd), 98 | ) 99 | 100 | # command from command dict 101 | elif self.command_dict.match(self.commandlower): 102 | matched_cmd = self.command_dict.get_last_matched_command() 103 | await self._os_cmd( 104 | cmd=self.command_dict.get_cmd(matched_cmd), 105 | args=self.args, 106 | markdown_convert=self.command_dict.get_opt_markdown_convert(matched_cmd), 107 | formatted=self.command_dict.get_opt_formatted(matched_cmd), 108 | code=self.command_dict.get_opt_code(matched_cmd), 109 | split=self.command_dict.get_opt_split(matched_cmd), 110 | ) 111 | 112 | else: 113 | await self._unknown_command() 114 | 115 | async def _show_help(self): 116 | """Show the help text.""" 117 | if not self.args: 118 | response = ( 119 | "Hello, I am your bot! " 120 | "Use `help all` or `help commands` to view " 121 | "available commands." 122 | ) 123 | await send_text_to_room(self.client, self.room.room_id, response) 124 | return 125 | 126 | topic = self.args[0] 127 | if topic == "rules": 128 | response = "These are the rules: Act responsibly." 129 | 130 | elif topic == "commands" or topic == "all": 131 | if not self.command_dict.is_empty(): 132 | response = "Available commands:\n" 133 | for icom in self.command_dict: 134 | response += f"\n- {icom}: {self.command_dict.get_help(icom)}" 135 | await send_text_to_room( 136 | self.client, 137 | self.room.room_id, 138 | response, 139 | markdown_convert=True, 140 | formatted=True, 141 | code=False, 142 | split=None, 143 | ) 144 | 145 | else: 146 | response = "Your command dictionary seems to be empty!" 147 | 148 | return 149 | else: 150 | response = f"Unknown help topic `{topic}`!" 151 | await send_text_to_room(self.client, self.room.room_id, response) 152 | 153 | async def _unknown_command(self): 154 | await send_text_to_room( 155 | self.client, 156 | self.room.room_id, 157 | ( 158 | f"{self.command}\n" 159 | "Try the *help* command for more information." 160 | ), 161 | split="\n", 162 | ) 163 | 164 | async def _os_cmd( 165 | self, 166 | cmd: str, 167 | args: list, 168 | markdown_convert=True, 169 | formatted=True, 170 | code=False, 171 | split=None, 172 | ): 173 | """Pass generic command on to the operating system. 174 | 175 | cmd (str): string of the command including any path, 176 | make sure command is found 177 | by operating system in its PATH for executables 178 | e.g. "date" for OS date command. 179 | cmd does not include any arguments. 180 | Valid example of cmd: "date" 181 | Invalid example for cmd: "echo 'Date'; date --utc" 182 | Invalid example for cmd: "echo 'Date' && date --utc" 183 | Invalid example for cmd: "TZ='America/Los_Angeles' date" 184 | If you have commands that consist of more than 1 command, 185 | put them into a shell or .bat script and call that script 186 | with any necessary arguments. 187 | args (list): list of arguments 188 | Valid example: [ '--verbose', '--abc', '-d="hello world"'] 189 | markdown_convert (bool): value for how to format response 190 | formatted (bool): value for how to format response 191 | code (bool): value for how to format response 192 | """ 193 | try: 194 | # create a combined argv list, e.g. ['date', '--utc'] 195 | argv_list = [cmd] 196 | if args is not None: 197 | argv_list += args 198 | 199 | logger.debug( 200 | f'OS command "{argv_list[0]}" with ' f'args: "{argv_list[1:]}"' 201 | ) 202 | 203 | # Set environment variables for the subprocess here. 204 | # Env variables like PATH, etc. are already set. In order to not lose 205 | # any set env variables we must merge existing env variables with the 206 | # new env variable(s). subprocess.Popen must be called with the 207 | # complete combined list. 208 | new_env = os.environ.copy() 209 | new_env['ENO_SENDER'] = self.event.sender 210 | new_env['ENO_TIMESTAMP_SENT'] = str(int(self.event.server_timestamp / 1000)) 211 | new_env['ENO_TIMESTAMP_RECEIVED'] = time.strftime("%s") 212 | 213 | run = subprocess.Popen( 214 | argv_list, # list of argv 215 | stdin=subprocess.PIPE, 216 | stdout=subprocess.PIPE, 217 | stderr=subprocess.PIPE, 218 | universal_newlines=True, 219 | env=new_env, 220 | ) 221 | run.stdin.write( self.command ) 222 | output, std_err = run.communicate() 223 | output = output.strip() 224 | std_err = std_err.strip() 225 | if run.returncode != 0: 226 | logger.debug( 227 | f"Bot command {cmd} exited with return " 228 | f"code {run.returncode} and " 229 | f'stderr as "{std_err}" and ' 230 | f'stdout as "{output}"' 231 | ) 232 | output = ( 233 | f"*** Error: command {cmd} returned error " 234 | f"code {run.returncode}. ***\n{std_err}\n{output}" 235 | ) 236 | response = output 237 | except Exception: 238 | response = SERVER_ERROR_MSG + traceback.format_exc() 239 | code = True # format stack traces as code 240 | logger.debug(f"Sending this reply back: {response}") 241 | await send_text_to_room( 242 | self.client, 243 | self.room.room_id, 244 | response, 245 | markdown_convert=markdown_convert, 246 | formatted=formatted, 247 | code=code, 248 | split=split, 249 | ) 250 | 251 | 252 | # EOF 253 | -------------------------------------------------------------------------------- /chat_functions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | r"""chat_functions.py. 4 | 5 | 0123456789012345678901234567890123456789012345678901234567890123456789012345678 6 | 0000000000111111111122222222223333333333444444444455555555556666666666777777777 7 | 8 | # chat_functions.py 9 | 10 | This file implements utility functions for 11 | - sending text messages 12 | - sending images 13 | - sending of other files like audio, video, text, PDFs, .doc, etc. 14 | 15 | Don't change tabbing, spacing, or formating as the 16 | file is automatically linted and beautified. 17 | 18 | """ 19 | 20 | import logging 21 | import os 22 | import traceback 23 | 24 | import aiofiles.os 25 | import magic 26 | from markdown import markdown 27 | from nio import SendRetryError, UploadResponse 28 | from PIL import Image 29 | 30 | logger = logging.getLogger(__name__) 31 | 32 | 33 | async def send_text_to_room( 34 | client, 35 | room_id, 36 | message, 37 | notice=True, 38 | markdown_convert=True, 39 | formatted=True, 40 | code=False, 41 | split=None, 42 | ): 43 | """Send text to a matrix room. 44 | 45 | Arguments: 46 | --------- 47 | client (nio.AsyncClient): The client to communicate with Matrix 48 | 49 | room_id (str): The ID of the room to send the message to 50 | 51 | message (str): The message content 52 | 53 | notice (bool): Whether the message should be sent with an 54 | "m.notice" message type (will not ping users) 55 | 56 | markdown_convert (bool): Whether to convert the message content 57 | to markdown. Defaults to true. 58 | 59 | formatted (bool): whether message should be sent as formatted message. 60 | Defaults to True. 61 | 62 | code (bool): whether message should be sent as code block with 63 | fixed-size font. 64 | If set to True, markdown_convert will be ignored. 65 | Defaults to False 66 | 67 | split (str): if set, split the message into multiple messages wherever 68 | the string specified in split occurs 69 | Defaults to None 70 | 71 | """ 72 | logger.debug(f"send_text_to_room {room_id} {message}") 73 | messages = [] 74 | if split: 75 | for paragraph in message.split(split): 76 | # strip again to get get rid of leading/trailing newlines and 77 | # whitespaces left over from previous split 78 | if paragraph.strip() != "": 79 | messages.append(paragraph) 80 | else: 81 | messages.append(message) 82 | 83 | for message in messages: 84 | # Determine whether to ping room members or not 85 | msgtype = "m.notice" if notice else "m.text" 86 | 87 | content = { 88 | "msgtype": msgtype, 89 | "body": message, 90 | } 91 | 92 | if formatted: 93 | content["format"] = "org.matrix.custom.html" 94 | 95 | if code: 96 | content["formatted_body"] = "
" + message + "\n
\n"
97 | # next line: work-around for Element on Android
98 | content["body"] = "```\n" + message + "\n```" # to format it as code
99 | elif markdown_convert:
100 | content["formatted_body"] = markdown(message)
101 |
102 | try:
103 | await client.room_send(
104 | room_id, "m.room.message", content, ignore_unverified_devices=True,
105 | )
106 | except SendRetryError:
107 | logger.exception(f"Unable to send message response to {room_id}")
108 |
109 |
110 | async def send_image_to_room(client, room_id, image):
111 | """Send image to single room.
112 |
113 | Arguments:
114 | ---------
115 | client (nio.AsyncClient): The client to communicate with Matrix
116 | room_id (str): The ID of the room to send the message to
117 | image (str): file name/path of image
118 |
119 | """
120 | logger.debug(f"send_image_to_room {room_id} {image}")
121 | await send_image_to_rooms(client, [room_id], image)
122 |
123 |
124 | async def send_image_to_rooms(client, rooms, image):
125 | """Send image to multiple rooms.
126 |
127 | Arguments:
128 | ---------
129 | client (nio.AsyncClient): The client to communicate with Matrix
130 | rooms (list): list of room_id-s
131 | image (str): file name/path of image
132 |
133 | This is a working example for a JPG image.
134 | "content": {
135 | "body": "someimage.jpg",
136 | "info": {
137 | "size": 5420,
138 | "mimetype": "image/jpeg",
139 | "thumbnail_info": {
140 | "w": 100,
141 | "h": 100,
142 | "mimetype": "image/jpeg",
143 | "size": 2106
144 | },
145 | "w": 100,
146 | "h": 100,
147 | "thumbnail_url": "mxc://example.com/SomeStrangeThumbnailUriKey"
148 | },
149 | "msgtype": "m.image",
150 | "url": "mxc://example.com/SomeStrangeUriKey"
151 | }
152 |
153 | """
154 | if not rooms:
155 | logger.info(
156 | "No rooms are given. This should not happen. "
157 | "This file is being droppend and NOT sent."
158 | )
159 | return
160 | if not os.path.isfile(image):
161 | logger.debug(
162 | f"File {image} is not a file. Doesn't exist or "
163 | "is a directory."
164 | "This file is being droppend and NOT sent."
165 | )
166 | return
167 |
168 | mime_type = magic.from_file(image, mime=True) # e.g. "image/jpeg"
169 | if not mime_type.startswith("image/"):
170 | logger.debug("Drop message because file does not have an image mime type.")
171 | return
172 |
173 | im = Image.open(image)
174 | (width, height) = im.size # im.size returns (width,height) tuple
175 |
176 | # first do an upload of image, then send URI of upload to room
177 | file_stat = await aiofiles.os.stat(image)
178 | async with aiofiles.open(image, "r+b") as f:
179 | resp, maybe_keys = await client.upload(
180 | f,
181 | content_type=mime_type, # image/jpeg
182 | filename=os.path.basename(image),
183 | filesize=file_stat.st_size,
184 | )
185 | if isinstance(resp, UploadResponse):
186 | logger.debug("Image was uploaded successfully to server. ")
187 | else:
188 | logger.debug(f"Failed to upload image. Failure response: {resp}")
189 |
190 | content = {
191 | "body": os.path.basename(image), # descriptive title
192 | "info": {
193 | "size": file_stat.st_size,
194 | "mimetype": mime_type,
195 | "thumbnail_info": None, # TODO
196 | "w": width, # width in pixel
197 | "h": height, # height in pixel
198 | "thumbnail_url": None, # TODO
199 | },
200 | "msgtype": "m.image",
201 | "url": resp.content_uri,
202 | }
203 |
204 | try:
205 | for room_id in rooms:
206 | await client.room_send(
207 | room_id, message_type="m.room.message", content=content
208 | )
209 | logger.debug(f'This image was sent: "{image}" to room "{room_id}".')
210 | except Exception:
211 | logger.debug(
212 | f"Image send of file {image} failed. " "Sorry. Here is the traceback."
213 | )
214 | logger.debug(traceback.format_exc())
215 |
216 |
217 | async def send_file_to_room(client, room_id, file):
218 | """Send file to single room.
219 |
220 | Arguments:
221 | ---------
222 | client (nio.AsyncClient): The client to communicate with Matrix
223 | room_id (str): The ID of the room to send the file to
224 | file (str): file name/path of file
225 |
226 | """
227 | logger.debug(f"send_file_to_room {room_id} {file}")
228 | await send_file_to_rooms(client, [room_id], file)
229 |
230 |
231 | async def send_file_to_rooms(client, rooms, file):
232 | """Send file to multiple rooms.
233 |
234 | Upload file to server and then send link to rooms.
235 | Works and tested for .pdf, .txt, .ogg, .wav.
236 | All these file types are treated the same.
237 |
238 | Do not use this function for images.
239 | Use the send_image_to_room() function for images.
240 |
241 | Matrix has types for audio and video (and image and file).
242 | See: "msgtype" == "m.image", m.audio, m.video, m.file
243 |
244 | Arguments:
245 | ---------
246 | client (nio.AsyncClient): The client to communicate with Matrix
247 | room_id (str): The ID of the room to send the file to
248 | rooms (list): list of room_id-s
249 | file (str): file name/path of file
250 |
251 | This is a working example for a PDF file.
252 | It can be viewed or downloaded from:
253 | https://matrix.example.com/_matrix/media/r0/download/
254 | example.com/SomeStrangeUriKey # noqa
255 | {
256 | "type": "m.room.message",
257 | "sender": "@someuser:example.com",
258 | "content": {
259 | "body": "example.pdf",
260 | "info": {
261 | "size": 6301234,
262 | "mimetype": "application/pdf"
263 | },
264 | "msgtype": "m.file",
265 | "url": "mxc://example.com/SomeStrangeUriKey"
266 | },
267 | "origin_server_ts": 1595100000000,
268 | "unsigned": {
269 | "age": 1000,
270 | "transaction_id": "SomeTxId01234567"
271 | },
272 | "event_id": "$SomeEventId01234567789Abcdef012345678",
273 | "room_id": "!SomeRoomId:example.com"
274 | }
275 |
276 | """
277 | if not rooms:
278 | logger.info(
279 | "No rooms are given. This should not happen. "
280 | "This file is being droppend and NOT sent."
281 | )
282 | return
283 | if not os.path.isfile(file):
284 | logger.debug(
285 | f"File {file} is not a file. Doesn't exist or "
286 | "is a directory."
287 | "This file is being droppend and NOT sent."
288 | )
289 | return
290 |
291 | # # restrict to "txt", "pdf", "mp3", "ogg", "wav", ...
292 | # if not re.match("^.pdf$|^.txt$|^.doc$|^.xls$|^.mobi$|^.mp3$",
293 | # os.path.splitext(file)[1].lower()):
294 | # logger.debug(f"File {file} is not a permitted file type. Should be "
295 | # ".pdf, .txt, .doc, .xls, .mobi or .mp3 ... "
296 | # f"[{os.path.splitext(file)[1].lower()}]"
297 | # "This file is being droppend and NOT sent.")
298 | # return
299 |
300 | # 'application/pdf' "plain/text" "audio/ogg"
301 | mime_type = magic.from_file(file, mime=True)
302 | # if ((not mime_type.startswith("application/")) and
303 | # (not mime_type.startswith("plain/")) and
304 | # (not mime_type.startswith("audio/"))):
305 | # logger.debug(f"File {file} does not have an accepted mime type. "
306 | # "Should be something like application/pdf. "
307 | # f"Found mime type {mime_type}. "
308 | # "This file is being droppend and NOT sent.")
309 | # return
310 |
311 | # first do an upload of file, see upload() in documentation
312 | # http://matrix-nio.readthedocs.io/en/latest/nio.html#nio.AsyncClient.upload
313 | # then send URI of upload to room
314 |
315 | file_stat = await aiofiles.os.stat(file)
316 | async with aiofiles.open(file, "r+b") as f:
317 | resp, maybe_keys = await client.upload(
318 | f,
319 | content_type=mime_type, # application/pdf
320 | filename=os.path.basename(file),
321 | filesize=file_stat.st_size,
322 | )
323 | if isinstance(resp, UploadResponse):
324 | logger.debug(f"File was uploaded successfully to server. Response is: {resp}")
325 | else:
326 | logger.info(
327 | "Bot failed to upload. "
328 | "Please retry. This could be temporary issue on your server. "
329 | "Sorry."
330 | )
331 | logger.info(
332 | f'file="{file}"; mime_type="{mime_type}"; '
333 | f'filessize="{file_stat.st_size}"'
334 | f"Failed to upload: {resp}"
335 | )
336 |
337 | # determine msg_type:
338 | if mime_type.startswith("audio/"):
339 | msg_type = "m.audio"
340 | elif mime_type.startswith("video/"):
341 | msg_type = "m.video"
342 | else:
343 | msg_type = "m.file"
344 |
345 | content = {
346 | "body": os.path.basename(file), # descriptive title
347 | "info": {"size": file_stat.st_size, "mimetype": mime_type,}, # noqa
348 | "msgtype": msg_type,
349 | "url": resp.content_uri,
350 | }
351 |
352 | try:
353 | for room_id in rooms:
354 | await client.room_send(
355 | room_id, message_type="m.room.message", content=content
356 | )
357 | logger.debug(f'This file was sent: "{file}" to room "{room_id}".')
358 | except Exception:
359 | logger.debug(f"File send of file {file} failed. Sorry. Here is the traceback.")
360 | logger.debug(traceback.format_exc())
361 |
--------------------------------------------------------------------------------
/command_dict.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import os
3 | import logging
4 | import yaml
5 | import re # regular expression matching
6 |
7 | logger = logging.getLogger(__name__)
8 |
9 |
10 | class CommandDictSanityError(Exception):
11 | pass
12 |
13 |
14 | class CommandDict:
15 |
16 | # Default formatting options
17 | # Mirror the defaults in the definition of the send_text_to_room
18 | # function in chat_functions.py which in turn mirror
19 | # the defaults of the Matrix API.
20 | DEFAULT_OPT_MARKDOWN_CONVERT = True
21 | DEFAULT_OPT_FORMATTED = True
22 | DEFAULT_OPT_CODE = False
23 | DEFAULT_OPT_SPLIT = None
24 |
25 | def __init__(self, command_dict_filepath):
26 | """Initialize command dictionary.
27 |
28 | Arguments:
29 | ---------
30 | command_dict (str): Path to command dictionary.
31 |
32 | """
33 |
34 | self.command_dict = None
35 | self.commands = {}
36 |
37 | self._last_matched_command = None
38 |
39 | self.load(command_dict_filepath)
40 |
41 | self.assert_sanity()
42 |
43 | return
44 |
45 | def __contains__(self, command):
46 | return command in self.commands.keys()
47 |
48 | def __getitem__(self, item):
49 | return self.commands[item]
50 |
51 | def __iter__(self):
52 | return self.commands.__iter__()
53 |
54 | def load(self, command_dict_filepath):
55 | """Try loading the command dictionary.
56 |
57 | Arguments:
58 | ----------
59 | command_dict_filepath (string): path to command dictionary.
60 |
61 | """
62 | try:
63 | with open(command_dict_filepath) as fobj:
64 | logger.debug(f"Loading command dictionary at {command_dict_filepath}")
65 | self.command_dict = yaml.safe_load(fobj.read())
66 |
67 | if "commands" in self.command_dict.keys():
68 | self.commands = self.command_dict["commands"]
69 |
70 | if "paths" in self.command_dict.keys():
71 | os.environ["PATH"] = os.pathsep.join(self.command_dict["paths"]+[os.environ["PATH"]])
72 | logger.debug(f'Path modified. Now: {os.environ["PATH"]}.')
73 |
74 | except FileNotFoundError:
75 | logger.error(f"File not found: {command_dict_filepath}")
76 |
77 | return
78 |
79 | def is_empty(self):
80 | """Returns whether there are commands in the dictionary.
81 |
82 | """
83 | return len(self.commands) == 0
84 |
85 | def assert_sanity(self):
86 | """Raises a CommandDictSanityError exception if the command dictionary
87 | is not considered "sane".
88 |
89 | """
90 | # Maybe in the future: Check whether commands can be found in path
91 | # For now, let the OS handle this
92 |
93 | # Check whether command dictionary has a correct structure. Namely,
94 | # that:
95 | #
96 | # 1. Toplevel children may only be called "commands" or "paths".
97 | if len(self.command_dict) > 2:
98 | raise CommandDictSanityError("Only two toplevel children allowed.")
99 | for key in self.command_dict.keys():
100 | if key not in ("commands","paths"):
101 | raise CommandDictSanityError(
102 | f"Invalid toplevel child found: {key}.")
103 | # 2. "paths" node must be a list, and must only contain string
104 | # children.
105 | if "paths" in self.command_dict:
106 | if type(self.command_dict["paths"]) != list:
107 | raise CommandDictSanityError(
108 | "The \"paths\" node must be a list.")
109 | for path in self.command_dict["paths"]:
110 | if type(path) != str:
111 | raise CommandDictSanityError("Defined paths must be strings.")
112 | # 3. "commands" node chilren (henceforth command nodes) must be
113 | # dictionaries,
114 | # 4. and may contain only the following keys:
115 | # "regex", "cmd", "help", "markdown_convert", "formatted",
116 | # "code" and "split".
117 | # 5. The command node children may only be strings.
118 | # 6. Command node children with keys "markdown_convert",
119 | # "formatted" or "code" may only be defined as "true" or as
120 | # "false".
121 | if "commands" in self.command_dict.keys():
122 | for com in self.command_dict["commands"]:
123 | # Implement rule 3
124 | if type(self.command_dict["commands"][com]) != dict:
125 | raise CommandDictSanityError(
126 | "Defined commands must be dictionaries.")
127 | for opt in self.command_dict["commands"][com].keys():
128 | # Implement rule 4
129 | if opt not in ("regex",
130 | "cmd",
131 | "help",
132 | "markdown_convert",
133 | "formatted",
134 | "code",
135 | "split"):
136 | raise CommandDictSanityError(
137 | f"In command \"{com}\", invalid option found: " \
138 | f"\"{opt}\".")
139 | # Implement rule 6
140 | elif opt in ("markdown_convert", "formatted", "code"):
141 | if type(self.command_dict["commands"][com][opt]) != bool:
142 | raise CommandDictSanityError(
143 | f"In command \"{com}\", invalid value for option "
144 | f"\"{opt}\" found: " \
145 | f"\"{self.command_dict['commands'][com][opt]}\"")
146 | # Implement rule 5
147 | else:
148 | if type(self.command_dict["commands"][com][opt]) != str:
149 | raise CommandDictSanityError(
150 | f"In command \"{com}\", command option " \
151 | f"\"{opt}\" must be a string.")
152 |
153 | return
154 |
155 | def match(self, string):
156 | """Returns whether the given string matches any of the commands' names
157 | regex patterns.
158 |
159 | Arguments:
160 | ----------
161 | string (str): string to match
162 |
163 | """
164 | matched = False
165 | cmd = None
166 |
167 | if string in self.commands.keys():
168 | matched = True
169 | cmd = string
170 |
171 | else:
172 | for command in self.commands.keys():
173 | if "regex" in self.commands[command].keys() \
174 | and re.match(self.commands[command]["regex"], string):
175 | matched = True
176 | cmd = command
177 | break
178 |
179 | self._last_matched_command = cmd
180 |
181 | return matched
182 |
183 | def get_last_matched_command(self):
184 | return self._last_matched_command
185 |
186 | def get_cmd(self, command):
187 | """Return the name of the executable associated with the given command,
188 | for the system to call.
189 |
190 | Arguments:
191 | ----------
192 | command (str): Name of the command in the command dictionary
193 |
194 | """
195 | return self.commands[command]["cmd"]
196 |
197 | def get_help(self,command):
198 | """Return the help string of the given command.
199 |
200 | Arguments:
201 | ----------
202 | command (str): Name of the command in the command dictionary
203 |
204 | """
205 | if "help" in self.commands[command]:
206 | return self.commands[command]["help"]
207 | else:
208 | return "No help defined for this command."
209 |
210 | def get_opt_markdown_convert(self, command):
211 | """Return boolean of the "markdown_convert" option.
212 |
213 | Arguments:
214 | ----------
215 | command (str): Name of the command in the command dictionary
216 |
217 | """
218 | if "markdown_convert" in self.command_dict["commands"][command].keys():
219 | return self.command_dict["commands"][command]["markdown_convert"]
220 | else:
221 | return CommandDict.DEFAULT_OPT_MARKDOWN_CONVERT
222 |
223 | def get_opt_formatted(self, command):
224 | """Return boolean of the "formatted" option.
225 |
226 | Arguments:
227 | ----------
228 | command (str): Name of the command in the command dictionary
229 |
230 | """
231 | if "formatted" in self.command_dict["commands"][command].keys():
232 | return self.command_dict["commands"][command]["formatted"]
233 | else:
234 | return CommandDict.DEFAULT_OPT_FORMATTED
235 |
236 | def get_opt_code(self, command):
237 | """Return boolean of the "code" option.
238 |
239 | Arguments:
240 | ----------
241 | command (str): Name of the command in the command dictionary
242 |
243 | """
244 | if "code" in self.command_dict["commands"][command].keys():
245 | return self.command_dict["commands"][command]["code"]
246 | else:
247 | return CommandDict.DEFAULT_OPT_CODE
248 |
249 | def get_opt_split(self, command):
250 | """Return the string defined in the "split" option, or None.
251 |
252 | Arguments:
253 | ----------
254 | command (str): Name of the command in the command dictionary
255 |
256 | """
257 | if "split" in self.command_dict["commands"][command].keys():
258 | return self.command_dict["commands"][command]["split"]
259 | else:
260 | return CommandDict.DEFAULT_OPT_SPLIT
261 |
262 |
--------------------------------------------------------------------------------
/commands.yaml.example:
--------------------------------------------------------------------------------
1 | #
2 | # ***** Initialization *****
3 | # This is an example command dictionary configuration. Mind you, this is an
4 | # example configuration meant to convey the structure of the configuration
5 | # file. It will probably not work as is in your environment.
6 | # So, copy this file to "commands.yaml". Then modify and adjust
7 | # "commands.yaml" to suit your needs. E.g. On Linux you would do
8 |
9 | # $ cp commands.yaml.example commands.yaml
10 | # $ nano commands.yaml # adjust to your needs, especially the paths!
11 |
12 |
13 | # ***** Paths *****
14 |
15 | # The bot needs to find your commands. For that the bot uses the PATH
16 | # variable. By adjusting the "paths" values below you can add additional
17 | # paths to the system PATH variable. If the bot is using commands that
18 | # are not already on the system PATH, you must add the additional command
19 | # paths below.
20 | # Furthermore, make sure the bot has read and execute access
21 | # to the commands, otherwise they won't work.
22 | # Here we show two example command paths. Adjust them as needed.
23 | paths:
24 | - /home/YOURUSER/.local/lib/matrix-eno-bot/eno/scripts
25 | - /home/YOURUSER/.local/etc/matrix-eno-bot/bin
26 |
27 |
28 | # ***** Commands *****
29 |
30 | # Commands are a list of specifications for your bot commands.
31 | # The children of the "commands" key are the command names.
32 | # Command names are nick names like "alert" or "backup".
33 | # Each command, listed by its name, has the following
34 | # keys (or properties):
35 | #
36 | # cmd: string: name of the command, executable, or script.
37 | # The bot will attempt to execute this command when
38 | # triggered. The bot will call this "cmd" and pass
39 | # the given arguments to it.
40 | # Example: "date" (a command provided by the OS)
41 | # help: string: help string explaining the command.
42 | # This is useful when you forget how use the command.
43 | # The help string provided will be listed and shown with
44 | # the 'help' command of the bot.
45 | # Example: "returns the current date of the server"
46 | # regex: string: regex pattern whose matches are valid ways to call the
47 | # command.
48 | # If a bot message matches a regular expression, then the
49 | # corresponding command will be executed.
50 | # If a bot message matched multiple regular expressions,
51 | # only the first matching command will be executed!
52 | # It is recommended that the regular expressions are used
53 | # such that they are mutually exclusive to avoid
54 | # situation where one messages matches multiple regular
55 | # expressions.
56 | # When the "regex" matches the "cmd" will be triggered.
57 | # Example: "^date$" (only the exact string of "date"
58 | # will match)
59 | # markdown_convert: true or false: Specifies whether the message reply has
60 | # been formatted in markdown.
61 | # The bot will convert this markdown-formatted input
62 | # and convert it into an HTML-like format understood by
63 | # Matrix, so that the bot reply shows up visually as
64 | # nice as the markdown input.
65 | # Defaults to true.
66 | # Example: Your command output is a markdown formatted
67 | # string such as "I *really* like it!" Set
68 | # markdown_convert to true and the receiver gets the
69 | # text "I really like it!" with the word "really"
70 | # visually in italic. In short, use "true", whenever
71 | # your command output is a markdown-formatted string;
72 | # false otherwise.
73 | # formatted: true or false: Specifies whether message reply will
74 | # be sent as a formatted message.
75 | # Defaults to true.
76 | # code: true or false: Specifies whether message reply will
77 | # be formatted as a code block with fixed-size font.
78 | # If set to "true", "markdown_convert" will be ignored.
79 | # Defaults to false.
80 | # split: string: if this string is set, splits the message reply
81 | # into multiple messages wherever the string specified in
82 | # split occurs.
83 | # Defaults to None, no message splitting by default.
84 | # Example: "\n\n\n" (Wherever the command output contains
85 | # two empty lines, the output will be split. Each piece
86 | # will be sent as a separate reply message.
87 | commands:
88 |
89 | # There are 3 kinds of commands.
90 | # a) built-in commands
91 | # b) pre-packed commands provided by the matrix-eno-bot repo
92 | # c) your custom commands that you can add
93 | # To add your custom commands go to the end of file, you will
94 | # find a "Custom commands" header there. Add them there.
95 |
96 | # Built-in commands
97 | # ---------------
98 | # help : "help" is a reserved word, so don't use it as a custom command!
99 | # reload : "reload" is a reserved word, so don't use it as a custom command!
100 |
101 |
102 | # Pre-packed commands provided by the matrix-eno-bot repo
103 | # -------------------------------------------------------
104 |
105 | # alert if too many resources are used, best to use with cron
106 | alert:
107 | regex: "alert$|^alert .*$|^alarm$|^alarm .*|^alert.sh$"
108 | cmd: alert.sh
109 | help: shows if any CPU, RAM, or disk thresholds have been exceeded
110 | markdown_convert: false
111 | formatted: true
112 | code: true
113 | # perform a backup to disk
114 | backup:
115 | regex: "^backup$|^backup .*$|^backup.sh$"
116 | cmd: backup.sh
117 | help: performs backup on server
118 | markdown_convert: false
119 | formatted: true
120 | code: true
121 | # get BTC ticker
122 | btc:
123 | regex: "^btc$|^btc .*$|^bitcoin$|^btc.sh$"
124 | cmd: btc.sh
125 | help: gives Bitcoin BTC price info
126 | markdown_convert: false
127 | formatted: true
128 | code: true
129 | # get cheatsheets, see https://github.com/cheat/cheat
130 | cheatsheet:
131 | regex: "^cheat$|^cheatsheet$|^chuleta$|^cheat.sh$|^cheat .*$|^cheatsheet .*$|^chuleta .*$|^cheat.sh .*$"
132 | cmd: cheat
133 | help: get cheatsheets, see https://github.com/cheat/cheat
134 | markdown_convert: false
135 | formatted: true
136 | code: true
137 | # check status and look for updates
138 | # see also: upgrade
139 | check:
140 | regex: "^check$|^chk$|^status$|^state$|^check .*$|^chk .*|^status .*$|^state .*$|^check.sh$|^check.sh .*"
141 | cmd: check.sh
142 | help: check status, health status, updates, etc.
143 | markdown_convert: false
144 | formatted: true
145 | code: false
146 | # CPU temperature, to monitor the CPU temperatures
147 | cputemp:
148 | regex: "^cpu$|^temp$|^temperature$|^celsius$|^cputemp.*$|^hot$|^chaud$"
149 | cmd: cputemp.sh
150 | help: give the current CPU temperatures
151 | markdown_convert: false
152 | formatted: true
153 | code: false
154 | # get date and time
155 | datetime:
156 | regex: "^date$|^time$|^tiempo$|^hora$|^temps$|^heure$|^heures$|^datum$|^zeit$|^datetime.sh$"
157 | cmd: datetime.sh
158 | help: give current date and time of server
159 | markdown_convert: false
160 | formatted: true
161 | code: true
162 | # duckduckgo
163 | ddg:
164 | regex: "^ddg$|^ddg .*$|^duck$|^duck .*$|^duckduckgo$|^duckduckgo .*$|^search$|^search .*|^ddg.sh$|^ddg.sh .*"
165 | cmd: ddg.sh
166 | help: search the web with DuckDuckGo search
167 | markdown_convert: false
168 | formatted: true
169 | code: false
170 | # disk space, monitor disk space
171 | disks:
172 | regex: "^disks$|^disk$|^full$|^space$|^disks.sh$"
173 | cmd: disks.sh
174 | help: see how full your disks or mountpoints are
175 | markdown_convert: false
176 | formatted: true
177 | code: true
178 | # echo, trivial example to have the bot respond
179 | echo:
180 | regex: "^echo$|^echo .*"
181 | cmd: echo.py
182 | help: bot echoes back your input
183 | markdown_convert: false
184 | formatted: true
185 | code: false
186 | # get ETH ticker
187 | eth:
188 | regex: "^eth$|^eth .*$|^ethereum$|^eth.sh$"
189 | cmd: eth.sh
190 | help: gives Ethereum price info
191 | markdown_convert: false
192 | formatted: true
193 | code: true
194 | # get firewall settings
195 | firewall:
196 | regex: "^firewall$|^fw$|^firewall .*$|^firewall.sh$"
197 | cmd: firewall.sh
198 | help: list the firewall settings and configuration
199 | markdown_convert: false
200 | formatted: true
201 | code: true
202 | # get a compliment, hello
203 | hello:
204 | regex: "^salut$|^ciao$|^hallo$|^hi$|^servus$|^hola$|^hello$|^hello .*$|^bonjour$|^bonne nuit$|^hello.sh$"
205 | cmd: hello.sh
206 | help: gives you a friendly compliment
207 | markdown_convert: false
208 | formatted: true
209 | code: false
210 | # Hacker News
211 | hn:
212 | regex: "^hn$|^hn .*$|^hn.sh$|^hn.sh .*"
213 | cmd: hn.sh
214 | help: read Hacker News, fetches front page headlines from Hacker News
215 | markdown_convert: false
216 | formatted: true
217 | code: false
218 | # Messari News
219 | mn:
220 | regex: "^mn$|^mn .*$|^mn.sh$|^mn.sh .*"
221 | cmd: mn.sh
222 | help: read Messari News, fetches the latest news articles from Messari
223 | markdown_convert: false
224 | formatted: true
225 | code: false
226 | split: "\n\n\n"
227 | # message of the day
228 | motd:
229 | regex: "^motd|^motd .*|^motd.sh$"
230 | cmd: motd.sh
231 | help: gives you the Linux Message Of The Day
232 | # platform info
233 | platforminfo:
234 | regex: "^platform$|^platform .*|^platforminfo.py$"
235 | cmd: platforminfo.py
236 | help: give hardware and operating system platform information
237 | # ps, host status
238 | ps:
239 | regex: "^ps$|^ps .*|^ps.sh$"
240 | cmd: ps.sh
241 | help: print current CPU, RAM and Disk utilization of server
242 | markdown_convert: false
243 | formatted: true
244 | code: true
245 | # restart, reset
246 | restart:
247 | regex: "^restart$|^reset$|^restart .*$|^reset .*$|^restart.sh$|^restart.sh .*"
248 | cmd: restart.sh
249 | help: restart the bot itself, or Matrix services
250 | markdown_convert: false
251 | formatted: true
252 | code: false
253 | # RSS
254 | rss:
255 | regex: "^rss$|^feed$|^rss .*$|^feed .*$|^rss.sh$|^rss.sh .*"
256 | cmd: rss.sh
257 | help: read RSS feeds
258 | markdown_convert: false
259 | formatted: true
260 | code: false
261 | split: "\n\n\n"
262 | # Stock-to-flow
263 | s2f:
264 | regex: "^s2f$|^mys2f.py.*|^flow$|^s2f|^flow .*$|^s2f .$|^s-to-f$|^stock-to-flow .*$|^eyf$|^eyf .*$|^e-y-f$"
265 | cmd: s2f.sh
266 | help: give Stock-to-flow info
267 | markdown_convert: false
268 | formatted: true
269 | code: true
270 | # tides
271 | tides:
272 | regex: "^tide$|^tides$|^marea|^mareas|^tide .*$|^tides .*$|^marea .*$|^mareas .*$|^gehzeiten .*$|^tides.sh$|^tides.sh .*"
273 | cmd: tides.sh
274 | help: give tidal forecast
275 | markdown_convert: false
276 | formatted: true
277 | code: false
278 | # top CPU, MEM consumers
279 | top:
280 | regex: "^top$|^top .*|^top.sh$|^top.sh .*"
281 | cmd: top.sh
282 | help: list 5 top CPU and RAM consuming processes
283 | markdown_convert: false
284 | formatted: true
285 | code: true
286 | # get TOTP 2FA pin
287 | totp:
288 | regex: "^otp$|^totp$|^otp .*$|^totp .*$"
289 | cmd: totp.sh
290 | help: get 2FA Two-factor-authentication TOTP PIN via bot message
291 | markdown_convert: false
292 | formatted: true
293 | code: false
294 | # twitter
295 | twitter:
296 | regex: "^tweet$|^twitter$|^tweet .*$|^twitter .*$|^twitter.sh$|^twitter.sh .*"
297 | cmd: twitter.sh
298 | help: read latest user tweets from Twitter
299 | markdown_convert: false
300 | formatted: true
301 | code: false
302 | # update components
303 | update:
304 | regex: "^update$|^upgrade$|^update .*$|^upgrade .*$|^update.sh$|^update.sh .*"
305 | cmd: update.sh
306 | help: update operating sytem
307 | markdown_convert: false
308 | formatted: true
309 | code: false
310 | # list matrix users by issuing a REST API query
311 | users:
312 | regex: "^usr$|^user$|^users$|^users .*$|^users.sh$"
313 | cmd: users.sh
314 | help: list registered Matrix users
315 | markdown_convert: false
316 | formatted: true
317 | code: true
318 | # wake up PC via wake-on-LAN
319 | wake:
320 | regex: "^wake$|^wakeup$|^wake .*$|^wakeup .*$|^wakelan .*$|^wake.sh$|^wake .*"
321 | cmd: wake.sh
322 | help: wake up another PC via LAN
323 | markdown_convert: false
324 | formatted: true
325 | code: false
326 | # waves and surf conditions
327 | # see also: tides
328 | waves:
329 | regex: "^wave$|^waves$|^wave .*$|^waves .*$|^surf$|^surf .*$|^waves.sh$"
330 | cmd: waves.sh
331 | help: give waves and surf forecast
332 | markdown_convert: false
333 | formatted: true
334 | code: true
335 | # get weather forecast
336 | weather:
337 | regex: "^weather$|^tiempo$|^wetter$|^temps$|^weather .*$|^tiempo .*$|^eltiempo .*$|^wetter .*$|^temps .*$|^weather.sh$|^weather.sh .*"
338 | cmd: weather.sh
339 | help: give weather forecast
340 | markdown_convert: false
341 | formatted: true
342 | code: true
343 | # fetch web pages
344 | web:
345 | regex: "^www$|^web$|^web .*$|^www .*$|^browse$|^browse .*|^web.sh$|^web.sh .*"
346 | cmd: web.sh
347 | help: surf the web, get a web page (JavaScript pages not supported)
348 | markdown_convert: false
349 | formatted: true
350 | code: false
351 | # whoami
352 | whoami:
353 | regex: "^w$|^who$|^whoami$"
354 | cmd: whoami.py
355 | help: return information about the user, whose unix account is running the bot
356 | markdown_convert: false
357 | formatted: true
358 | code: false
359 |
360 |
361 | # Custom commands
362 | # ---------------
363 |
364 | # add your custom commands here
365 |
366 | # End of commands configuration file
367 |
--------------------------------------------------------------------------------
/config.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | r"""config.py.
4 |
5 | 0123456789012345678901234567890123456789012345678901234567890123456789012345678
6 | 0000000000111111111122222222223333333333444444444455555555556666666666777777777
7 |
8 | # config.py
9 |
10 | This file implements utility functions for
11 | - reading in the YAML config file
12 | - performing the according initialization and set-up
13 |
14 | Don't change tabbing, spacing, or formating as the
15 | file is automatically linted and beautified.
16 |
17 | """
18 |
19 | import logging
20 | import re
21 | import os
22 | import yaml
23 | import sys
24 | from typing import List, Any
25 | from errors import ConfigError
26 |
27 | logger = logging.getLogger()
28 |
29 |
30 | class Config(object):
31 | """Handle config file."""
32 |
33 | def __init__(self, filepath):
34 | """Initialize.
35 |
36 | Arguments:
37 | ---------
38 | filepath (str): Path to config file
39 |
40 | """
41 | if not os.path.isfile(filepath):
42 | raise ConfigError(f"Config file '{filepath}' does not exist")
43 |
44 | # Load in the config file at the given filepath
45 | with open(filepath) as file_stream:
46 | self.config = yaml.safe_load(file_stream.read())
47 |
48 | # Logging setup
49 | formatter = logging.Formatter(
50 | '%(asctime)s | %(name)s [%(levelname)s] %(message)s')
51 |
52 | log_level = self._get_cfg(["logging", "level"], default="INFO")
53 | logger.setLevel(log_level)
54 |
55 | file_logging_enabled = self._get_cfg(
56 | ["logging", "file_logging", "enabled"], default=False)
57 | file_logging_filepath = self._get_cfg(
58 | ["logging", "file_logging", "filepath"], default="bot.log")
59 | if file_logging_enabled:
60 | handler = logging.FileHandler(file_logging_filepath)
61 | handler.setFormatter(formatter)
62 | logger.addHandler(handler)
63 |
64 | console_logging_enabled = self._get_cfg(
65 | ["logging", "console_logging", "enabled"], default=True)
66 | if console_logging_enabled:
67 | handler = logging.StreamHandler(sys.stdout)
68 | handler.setFormatter(formatter)
69 | logger.addHandler(handler)
70 |
71 | # Storage setup
72 | self.database_filepath = self._get_cfg(
73 | ["storage", "database_filepath"], required=True)
74 | self.store_filepath = self._get_cfg(
75 | ["storage", "store_filepath"], required=True)
76 | self.command_dict_filepath = self._get_cfg(
77 | ["storage", "command_dict_filepath"], default=None)
78 | self.room_dict_filepath = self._get_cfg(
79 | ["storage", "room_dict_filepath"], default=None)
80 |
81 | # Create the store folder if it doesn't exist
82 | if not os.path.isdir(self.store_filepath):
83 | if not os.path.exists(self.store_filepath):
84 | os.mkdir(self.store_filepath)
85 | else:
86 | raise ConfigError(
87 | f"storage.store_filepath '{self.store_filepath}' is "
88 | "not a directory")
89 |
90 | # Matrix bot account setup
91 | self.user_id = self._get_cfg(["matrix", "user_id"], required=True)
92 | if not re.match("@.*:.*", self.user_id):
93 | raise ConfigError(
94 | "matrix.user_id must be in the form @name:domain")
95 |
96 | self.user_password = self._get_cfg(
97 | ["matrix", "user_password"], required=False, default=None)
98 | self.access_token = self._get_cfg(
99 | ["matrix", "access_token"], required=False, default=None)
100 | self.device_id = self._get_cfg(["matrix", "device_id"], required=True)
101 | self.device_name = self._get_cfg(
102 | ["matrix", "device_name"], default="nio-template")
103 | self.homeserver_url = self._get_cfg(
104 | ["matrix", "homeserver_url"], required=True)
105 |
106 | self.command_prefix = self._get_cfg(
107 | ["command_prefix"], default="!c") + " "
108 |
109 | if not self.user_password and not self.access_token:
110 | raise ConfigError(
111 | "Either user_password or access_token must be specified")
112 |
113 | self.trust_own_devices = self._get_cfg(
114 | ["matrix", "trust_own_devices"], default=False, required=False)
115 | self.change_device_name = self._get_cfg(
116 | ["matrix", "change_device_name"], default=False, required=False)
117 | self.process_audio = self._get_cfg(
118 | ["matrix", "process_audio"], default=False, required=False)
119 | self.accept_invitations = self._get_cfg(
120 | ["matrix", "accept_invitations"], default=True, required=False)
121 |
122 | def _get_cfg(
123 | self,
124 | path: List[str],
125 | default: Any = None,
126 | required: bool = True,
127 | ) -> Any:
128 | """Get a config option.
129 |
130 | Get a config option from a path and option name,
131 | specifying whether it is required.
132 |
133 | Raises
134 | ------
135 | ConfigError: If required is specified and the object is not found
136 | (and there is no default value provided),
137 | this error will be raised.
138 |
139 | """
140 | # Sift through the the config until we reach our option
141 | config = self.config
142 | for name in path:
143 | config = config.get(name)
144 |
145 | # If at any point we don't get our expected option...
146 | if config is None:
147 | # Raise an error if it was required, allow default to be None
148 | if required:
149 | raise ConfigError(
150 | f"Config option {'.'.join(path)} is required")
151 |
152 | # or return the default value
153 | return default
154 |
155 | # We found the option. Return it
156 | return config
157 |
--------------------------------------------------------------------------------
/config.yaml.example:
--------------------------------------------------------------------------------
1 | # Welcome to the sample config file
2 | # Below you will find various config sections and options
3 | # Default values are shown
4 |
5 | # The string to prefix messages with to talk to the bot in group chats
6 | # Changed from original "!c" to "1z! because on cell phone "!c" seems too
7 | # complicated.
8 | command_prefix: "1z"
9 |
10 | # Options for connecting to the bot's Matrix account
11 | matrix:
12 | # The Matrix User ID of the bot account
13 | user_id: "@bot:example.com"
14 | # Matrix account password
15 | user_password: ""
16 |
17 | # Matrix account access token
18 | # To create a new Matrix device for the bot, use and set the `user_password`
19 | # field. Once the device exists, you can optionally replace the
20 | # `user_password` with the `access_token` field. Using the `access_token`
21 | # field is slightly safer as it does not expose the password of the bot
22 | # account. You can use only one or the other: either use the `user_password`
23 | # or the `access_token` field. You can find the access token in the Matrix
24 | # client where you have registered the bot account or you can find it in
25 | # the bot log file. If the logging options are set accordingly the access
26 | # token will be logged to the bot log file.
27 | # Default: commented out
28 | # access_token: "PutYourLongAccessTokenHere"
29 |
30 | # The URL of the homeserver to connect to
31 | homeserver_url: https://example.com
32 | # The device ID that is **non pre-existing** device
33 | # If this device ID already exists, messages will be dropped
34 | # silently in encrypted rooms
35 | device_id: ABCDEFGHIJ
36 | # What to name the device? Often referred to as device name or display name.
37 | device_name: eno
38 | # Should the bot trust all the devices of its own Matrix account?
39 | # Default: false
40 | # If false, nothing is done. After login, no device will be automatically
41 | # trusted.
42 | # If true, once at startup, after logging in, the bot device will
43 | # automatically establish trust to all other devices of the bot account.
44 | trust_own_devices: false
45 | # Do you want to change the device_name of the already existing bot?
46 | # Default: false
47 | # If false, nothing is done. After creation, device_name will be ignored.
48 | # If true, device_name of bot will be changed to value given in device_name.
49 | change_device_name: false
50 | # encrytion is enabled by default
51 | accept_invitations: true
52 | # Should the bot accept invitations? If disabled then the bot must be
53 | # added to the desired rooms "manually". This configuration is mostly
54 | # useful in combination with the room-specific processing "feature"
55 | # (refer to storage.room_dict_path)
56 | process_audio: false
57 | # Should the bot also process audio messages (need to be downloaded
58 | # before processing)? If true the audio content will be passed (just
59 | # as with text content) to the handling command program, but base64
60 | # encoded inside a "data:" url
61 |
62 | storage:
63 | # The path to the database
64 | database_filepath: "bot.db"
65 | # The path to a directory for internal bot storage
66 | # containing encryption keys, sync tokens, etc.
67 | store_filepath: "./store"
68 | # The path to the command dictionary configuration file
69 | command_dict_filepath: "./commands.yaml"
70 | # The path to the room dictionary configuration file
71 | room_dict_filepath: "./room.yaml"
72 |
73 | # Logging setup
74 | logging:
75 | # Logging level
76 | # Allowed levels are 'INFO', 'WARNING', 'ERROR', 'DEBUG'
77 | # where DEBUG is most verbose
78 | level: INFO
79 | # Configure logging to a file
80 | file_logging:
81 | # Whether logging to a file is enabled
82 | enabled: false
83 | # The path to the file to log to. May be relative or absolute
84 | filepath: bot.log
85 | # Configure logging to the console output
86 | console_logging:
87 | # Whether logging to the console is enabled
88 | enabled: true
89 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | matrix-eno-bot:
5 | container_name: matrix-eno-bot
6 | image: 'matrix-eno-bot:latest'
7 | build: '.'
8 | restart: always
9 | volumes:
10 | - ${PWD}/config.yaml:/bot/config.yaml
11 | - ${PWD}/commands.yaml:/bot/commands.yaml
12 | - ${PWD}/bot.db:/bot/bot.db
13 | - ${PWD}/store/:/bot/store/
14 | stop_signal: SIGINT
15 |
--------------------------------------------------------------------------------
/eno/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | These are the files specific to the `eno` bot.
4 |
5 | It is important that you read the `matrix-eno-bot.service.example` to learn how you can run the `eno` bot as a service. The top lines are comments that give clear instructions.
6 |
--------------------------------------------------------------------------------
/eno/logos/Nsibidi-small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8go/matrix-eno-bot/e18c5cc2bbced2229f661a3b9a8bc022e2a7d808/eno/logos/Nsibidi-small.png
--------------------------------------------------------------------------------
/eno/logos/Nsibidi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8go/matrix-eno-bot/e18c5cc2bbced2229f661a3b9a8bc022e2a7d808/eno/logos/Nsibidi.png
--------------------------------------------------------------------------------
/eno/logos/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | The logos of the `eno` bot and related images.
4 |
--------------------------------------------------------------------------------
/eno/logos/eno-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8go/matrix-eno-bot/e18c5cc2bbced2229f661a3b9a8bc022e2a7d808/eno/logos/eno-logo.png
--------------------------------------------------------------------------------
/eno/logos/eno-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
204 |
--------------------------------------------------------------------------------
/eno/matrix-eno-bot.service.example:
--------------------------------------------------------------------------------
1 | # This is the systemd service file for the matrix-eno-bot
2 | # It helps you to run the bot as a system service
3 |
4 | # Installation of matrix-eno-bot as service:
5 | # cp matrix-eno-bot.service.example matrix-eno-bot.service
6 | # Modify/Configure the matrix-eno-bot.service to fit your needs
7 | # nano matrix-eno-bot.service
8 | # Copy your service file to system directory
9 | # sudo cp matrix-eno-bot.service /etc/systemd/system/
10 | # Enable it to create it:
11 | # sudo systemctl enable matrix-eno-bot
12 | # Start it:
13 | # sudo systemctl start matrix-eno-bot
14 | # Get status:
15 | # sudo systemctl status matrix-eno-bot
16 | # Restarting/reseting it:
17 | # sudo systemctl restart matrix-eno-bot
18 | # Stop it:
19 | # sudo systemctl stop matrix-eno-bot
20 | # To uninstall it:
21 | # sudo systemctl disable matrix-eno-bot
22 |
23 | [Unit]
24 | Description=matrix-eno-bot
25 |
26 | [Service]
27 | # change user name to fit your needs
28 | User=matrix-neo-bot
29 | Group=users
30 | Environment=PYTHONUNBUFFERED=1
31 | # change this to match your server's timezone
32 | Environment=TZ=UTC
33 | # change this PATH to fit your needs
34 | Environment=PATH=/home/matrix-eno-bot/matrix-eno-bot/eno/scripts:/home/matrix-eno-bot/bin:/home/matrix-eno-bot/.local/bin:/home/matrix-eno-bot/Scripts:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
35 | # change this PATH to fit your PATH
36 | ExecStart=/home/matrix-eno-bot/matrix-eno-bot/main.py /home/matrix-eno-bot/matrix-eno-bot/config.yaml
37 | ExecStop=/bin/kill -9 $MAINPID
38 | Restart=on-failure
39 | RestartSec=30
40 |
41 | [Install]
42 | WantedBy=multi-user.target
43 |
--------------------------------------------------------------------------------
/eno/scripts/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | This is te scripts directory of the `eno` bot. Most current scripts are `bash`, one is `Python3`.
4 | But these could be any scripts or programs in any language from `JavaScript` to `Go`.
5 |
6 | If you want to separate certain private variables (e.g. password hashes) from the scripts for security reasons, then have a look at the file `config.rc.example`. This is an optional file used (if available) by some scripts.
7 |
8 | A short desription of each provided script follows. But add your own scripts to improve your own bot!
9 |
10 | ## Bot as Personal Assistant: Example bot commands provided
11 |
12 | Commands useful to average users:
13 |
14 | - btc: gives Bitcoin BTC price info
15 | - ddg: search the web with DuckDuckGo search
16 | - eth: gives Ethereum price info
17 | - hello: gives you a friendly compliment
18 | - help: to list available bot commands
19 | - hn: read Hacker News, fetches front page headlines from Hacker News
20 | - mn: read Messari News, fetches the latest news articles from Messari
21 | - motd: gives you the Linux Message Of The Day
22 | - rss: read RSS feeds
23 | - s2f: print Bitcoin Stock-to-flow price info
24 | - tides: get today's low and high tides of your favorite beach
25 | - totp: get 2FA Two-factor-authentication TOTP One-Time-Password PIN via bot message (like Google Authenticator)
26 | - twitter: read latest user tweets from Twitter (does **not** work most of the time as info is scraped from web)
27 | - waves: get the surf report of your favorite beach
28 | - weather: get the weather forecast for your favorite city
29 | - web: surf the web, get a web page (JavaScript not supported)
30 |
31 | ## Bot as Admin Tool: Example bot commands provided to Matrix or system administrators
32 |
33 | With these commands a system administrator can maintain his Matrix installation and keep a watchful eye on his server all through the Matrix bot. Set the permissions accordingly in the config file to avoid unauthorized use of these bot commands!
34 |
35 | - alert: shows if any CPU, RAM, or Disk thresholds have been exceeded (best to combine with a cron job, and have the cron job send the bot message to Matrix admin rooms)
36 | - backup: runs your backup script to backup files, partitions, etc.
37 | - check: check status, health status, updates, etc. of bot, Matrix and the operating system
38 | - cputemp: monitor the CPU temperatures
39 | - date: gives date and time of server
40 | - disks: see how full your disks or mountpoints are
41 | - firewall: list the firewall settings and configuration
42 | - platform: gives hardware and operating system platform information
43 | - ps: print current CPU, RAM and Disk utilization of server
44 | - restart: restart the bot itself, or Matrix services
45 | - top: gives 5 top CPU and RAM consuming processes
46 | - update: update operating sytem and other software environments
47 | - users: list user accounts that exist on your server
48 | - wake: wake up other PCs on the network via wake-on-LAN
49 |
50 |
--------------------------------------------------------------------------------
/eno/scripts/affirmations.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | if [[ "$ENO_SENDER" != "" ]]; then
3 | PREFIX="${ENO_SENDER}, "
4 | else
5 | PREFIX=""
6 | fi
7 |
8 | MSG=$(echo "Le bonheur est mon droit imprescriptible. J’accepte que le bonheur devienne le cœur de mon existence.
9 | Je ressens de la joie et de la satisfaction en ce moment précis.
10 | Je me réveille chaque matin en me sentant heureux et enthousiaste à l’égard de ma vie.
11 | Je peux exploiter ma source de bonheur intérieur chaque fois que je le souhaite.
12 | En me permettant d’être heureux, j’incite les autres à être heureux aussi.
13 | Je m’amuse de tous mes efforts, même les plus banals.
14 | Je regarde le monde autour de moi et je ne peux qu’empêcher de sourire et de ressentir de la joie.
15 | Je trouve de la joie et du plaisir dans les choses les plus simples de la vie.
16 | J’ai le sens de l’humour et j’aime partager des rires avec les autres.
17 | Mon cœur déborde de joie.
18 | Mon partenaire et moi partageons un amour profond et puissant l’un pour l’autre.
19 | Je respecte et j’admire mon partenaire et je vois le meilleur en lui.
20 | J’aime mon partenaire exactement comment il est et j’apprécie ses qualités uniques.
21 | Mon partenaire et moi partageons de l’intimité émotionnelle chaque jour grâce à la conversation et au toucher.
22 | J’ai des limites saines avec mon partenaire.
23 | Mon partenaire et moi nous amusons ensemble et trouvons de nouvelles façons de profiter de notre temps ensemble.
24 | Mon partenaire et moi communiquons ouvertement et résolvons les conflits de manière pacifique et respectueuse.
25 | Je peux être entièrement moi-même et complètement authentique dans ma relation amoureuse.
26 | Je communique mes désirs et mes besoins de façon claire et en toute confiance avec mon partenaire.
27 | Je souhaite le meilleur à mon partenaire et je le soutiens dans chacune de ses entreprises.
28 | Je m’attends à réussir dans toutes mes entreprises, car le succès est mon état naturel.
29 | Je trouve facilement des solutions aux défis et aux obstacles et je les dépasse rapidement.
30 | Les erreurs et les contretemps sont des tremplins vers le succès, car j’apprends d’eux.
31 | Chaque jour, j’ai de plus en plus de succès.
32 | Je réussis ma vie maintenant, même si je travaille vers les succès futurs.
33 | Je sais exactement ce que je dois faire pour réussir.
34 | Je vois la peur comme un combustible pour ma réussite et je prends des risques malgré la peur.
35 | Je me sens puissant, capable, confiant, empli d’énergie et aux anges.
36 | J’ai l’intention de réussir et je sais que c’est une réalité qui m’attend à l’arrivée.
37 | J’ai maintenant atteint mon objectif _______ et je suis fier de ma réussite.
38 | Aujourd’hui, je réussis. Demain, je réussirai. Chaque jour, je réussis.
39 | Quand je respire, j’inspire la confiance et j’expire la timidité.
40 | J’adore rencontrer des étrangers et les aborder avec enthousiasme.
41 | Je vis au présent et je suis confiant dans l’avenir.
42 | Je fais preuve de confiance. Je suis audacieux et sociable.
43 | Je suis autonome, créatif et persévérant dans tout ce que je fais.
44 | Je suis énergique et enthousiaste. La confiance est ma seconde nature.
45 | Je n’attire que les meilleures opportunités et les personnes positives dans ma vie.
46 | Je suis un spécialiste en résolution de problème. Je me concentre sur les solutions et je trouve toujours la meilleure.
47 | J’aime le changement et je m’adapte facilement aux nouvelles situations.
48 | Je suis soigné, sain et confiant. Mon apparence est assortie à mon bien-être intérieur.
49 | Je développe ma confiance en moi. Rien n’est impossible et la vie est géniale.
50 | Je ne vois toujours que le bien chez les autres. J’attire uniquement des personnes confiantes et positives.
51 | Je m’accepte tel que je suis et je m’aime profondément et complètement.
52 | Je suis unique. Je me sens bien en étant vivant et en étant moi-même.
53 | Je me fais confiance et je sais que ma sagesse intérieure est mon meilleur guide.
54 | Je suis intègre. Je suis totalement fiable. Je fais ce que je dis.
55 | J’agis en toute sécurité.
56 | Je m’accepte pleinement et je sais que je suis digne des meilleures choses dans la vie.
57 | Je choisis d’être fier de moi-même.
58 | J’éprouve une profonde paix intérieure.
59 | Je me remplis l’esprit de pensées positives et nourrissantes.
60 | Confiance, estime de soi et sagesse intérieure croissent chaque jour.
61 | Mon système immunitaire est très fort et peut éliminer n’importe quel type de bactéries, de germes et de virus.
62 | Chaque cellule de mon corps vibre d’énergie et de santé.
63 | Je ne souffre pas d’aucune douleur, et mon corps est rempli d’énergie.
64 | Je nourris mon corps avec des aliments sains.
65 | Tous les organes de mon corps fonctionnent parfaitement.
66 | Mon corps guérit, et je me sens de mieux en mieux chaque jour.
67 | J’aime faire de l’exercice et renforcer mes muscles.
68 | À chaque respiration, je libère mon corps du stress.
69 | J’envoie de l’amour à tous les organes de mon corps.
70 | Je respire profondément, je fais régulièrement de l’exercice et je ne me nourris que d’aliments nutritifs pour mon corps.
71 | Je fais attention à ce dont mon corps a besoin pour la santé et la vitalité.
72 | Je dors profondément et paisiblement, et je me réveille reposé et plein d’énergie.
73 | Je suis entouré de personnes qui encouragent et soutiennent les choix sains.
74 | Mon univers est un endroit paisible, aimant et plein de joie.
75 | Je sème les germes de la paix partout où je vais.
76 | Je m’entoure de gens pacifiques.
77 | Mon milieu de travail est calme et paisible.
78 | J’inspire la paix, j’expire le chaos et le désordre.
79 | Ma maison est un sanctuaire paisible où je me sens en sécurité et heureux.
80 | Dans tout ce que je dis et fais, je choisis la paix.
81 | Je me libère des colères et des blessures passées et je me remplis de sérénité et de pensées pacifiques.
82 | La paix m’envahit maintenant et pour toujours.
83 | J’envoie de la paix dans le monde.
84 | Je réponds pacifiquement dans toutes les situations.
85 | Je m’appuie sur le moment présent.
86 | Je me concentre sur la tâche à accomplir.
87 | Tout va bien maintenant.
88 | Je suis reconnaissant pour ce moment et j’éprouve de la joie.
89 | Je reviens doucement et facilement au moment présent.
90 | J’observe mes pensées et mes actions sans les juger.
91 | Je suis pleinement présent dans toutes mes relations.
92 | La vie se passe en ce moment.
93 | J’accepte et j’accueille toutes les expériences, même désagréables.
94 | J’observe mes émotions sans m’y attacher.
95 | Je médite facilement sans résistance ni anxiété.
96 | Je me libère du passé et je vis pleinement le moment présent.
97 | La tranquillité m’envahit à chaque souffle profond que je prends.
98 | Chaque jour, je suis de plus en plus détendu.
99 | Être calme et détendu dynamise tout mon être.
100 | Tous les muscles de mon corps se relâchent et se relaxent.
101 | Toute la négativité et le stress s’évaporent de mon corps et de mon esprit.
102 | J’inspire la détente. J’expire le stress.
103 | Même quand il y a du chaos autour de moi, je reste calme et centré.
104 | Je dépasse le stress. Je vis en paix.
105 | Je suis dépourvu d’anxiété, et une profonde paix intérieure remplit mon esprit et mon corps.
106 | Tout va bien dans mon univers. Je suis calme., heureux et content.
107 | Je m’endors dans le bonheur, car je sais que tout va bien dans mon univers.
108 | « Je suis capable de tout »
109 | « Je peux le rêver donc je peux le faire »
110 | « J’ai en moi toutes les capacités pour réaliser mes projets et aller au bout de mes rêves »
111 | « Je suis douée, intelligente et capable de tout malgré ce que j’ai pu entendre et pensé »
112 | « J’ai décidé de m’aimer car c’est ma plus belle histoire d’amour et elle durera toute ma vie »
113 | « Aujourd’hui est le plus beau jour de ma vie car c’est le jour que je vis »
114 | « Je laisse le passé derrière moi et je sais qu’aujourd’hui sera une merveilleuse journée »
115 | « Les gens les plus heureux n’ont pas tout ce qu’il y a de mieux. Ils font juste de leur mieux avec tout ce qu’ils sont »
116 | « Chaque jour, de tous les points de vue, je vais de mieux en mieux »
117 | « Aujourd’hui, même s’il pleut, j’apprendrai à danser sous la pluie »
118 | « Parfois c’est différent de ce que tu espérais… et c’est mieux comme ça ! »
119 | « Quoi que tu fasses, fais ce qui te rend heureuse »
120 | « Chaque jour j’ai un peu plus de joie et de succès dans mon travail et dans ma vie »
121 | « Il n’est pas trop tard pour être ce que je veux être, mais c’est aujourd’hui que je dois m’y mettre »
122 | « J’accepte ce qui est, je laisse aller ce qui était et j’ai confiance en ce qui sera »
123 | « L’échec est simplement une façon de recommencer de manière plus intelligente »
124 | « Je n’ai pas besoin d’être parfait.e car la perfection n’existe pas. J’ai simplement besoin d’être et d’avancer »
125 | « Tant que je rêve, j’ose et je travaille, je suis capable d’y arriver »
126 | « Aujourd’hui est le seul jour pour réaliser mes rêves »
127 | « Je m’attends au meilleur pour lui donner toutes les chances d’apparaitre »
128 | J’habite un monde d’amour et d’acceptation.
129 | La vie m’aime et je suis en sécurité.
130 | Je parle avec sagesse et discernement.
131 | Je me réjouis de l’amour que j’ai à donner.
132 | C’est l’amour qui anime ma vie.
133 | Je reçois l’amour que je donne.
134 | Financièrement, je suis toujours à l’aise.
135 | L’amour inconditionnel c’est simplement un amour qui n’attend rien en retour.
136 | Je suis le créateur et l’acteur de ma vie.
137 | Les êtres sont comme des fleurs. Chacun à sa beauté propre, chacun s’ouvre et s’épanouit à sa manière et à son rythme.
138 | Toutes mes nouvelles habitudes m’aident de façon positive.
139 | Aujourd’hui je choisis de dépasser mes limites d’hier. Je suis prêt à m’ouvrir à quelque chose de nouveau.
140 | Je mets de l’amour dans mon regard et je vois tout clairement.
141 | Plus je comprends de choses et plus mon univers s’élargit.
142 | Je me fais toujours confiance.
143 | La manière dont nous voyons ce qu’il y a à l’extérieur de nous reflète ce qu’il y a en nous.
144 | Mon corps est un ami et j’en prends soin.
145 | Je me félicite pour les grandes et petites choses que je réalise.
146 | Je revendique mon pouvoir et je dépasse toutes mes limites.
147 | J’aime ce que je pense.
148 | Je donne à la Vie avec joie et la vie me le donne avec Amour.
149 | L’amour fait toujours disparaître la douleur.
150 | Les émotions sont des pensées qui sont actives dans notre corps.
151 | J’adore les enfants et les enfants m’adorent.
152 | M’aimer moi-même et les autres me permet de m’épanouir et de vivre au maximum de mes possibilités.
153 | Je m’aime dans toutes les expériences que je traverse et tout va bien.
154 | Je partage mes ressources et mon savoir avec la Vie.
155 | Je suis financièrement à l’aise.
156 | Mon corps est en paix, heureux, en bonne santé et moi aussi.
157 | En élargissant mes horizons, je fais facilement disparaître mes limites.
158 | Je suis une expression individuelle de la Vie.
159 | Je transforme les leçons à apprendre en partie de plaisir.
160 | Je fais des choix nouveaux, différents, plus positifs, qui me nourissent de l’intérieur.
161 | Ma maison et mon coeur sont des lieux de paix et de bonté.
162 | La seule chose que vous pouvez vraiment maîtriser c’est ce que vous pensez au moment présent.
163 | Vous avez tout pouvoir sur votre pensée du moment.
164 | Je me crée avec amour une santé parfaite.
165 | La sagesse que je cherche se trouve en moi.
166 | Chaque instant est un nouveau départ.
167 | Tout ce dont j’ai besoin est à portée de ma main.
168 | Je choisis un mode de vie paisible.
169 | Le pardon possède un pouvoir de guérison que j’ai toujours à ma disposition.
170 | Tout va bien. J’ai tout ce dont j’ai besoin en ce moment.
171 | Mes pensées déterminent ma vie.
172 | Je découvre maintenant de nouvelles et merveilleuses expériences. Je suis en sécurité.
173 | Je concentre doucement mon esprit sur les belles choses de la vie.
174 | Le pouvoir se trouve dans le moment présent. Affirmez votre pouvoir.
175 | Ma conscience est riche.
176 | Je me libère et je pardonne.
177 | C’est avec moi-même que j’entretiens la meilleure relation.
178 | J’ai la responsabilité de ma vie." | shuf -n1)
179 |
180 | echo "${PREFIX}$MSG"
181 |
182 | # EOF
183 |
--------------------------------------------------------------------------------
/eno/scripts/alert.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | MEM_ALERT=0.8 # more than 80% of RAM used
4 | CPU_ALERT=0.8 # more than 80% of CPU used, remember if there are 2 cores, this can go to 200%, 4 core to 400%
5 | DSK_ALERT=0.9 # more than 80% of DISK used
6 | TMP_ALERT=69 # 69C, temperature alert if above x C Celsius
7 |
8 | # free -m | awk 'NR==2{printf "Memory Usage: %s/%sMB (%.2f%%)\n", $3,$2,$3*100/$2 }'
9 | # df -h | awk '$NF=="/"{printf "Disk Usage: %d/%dGB (%s)\n", $3,$2,$5}'
10 | # top -bn1 | grep load | awk '{printf "CPU Load: %.2f\n", $(NF-2)}'
11 |
12 | # Memory Usage: 536/3793MB (14.13%)
13 | # Disk Usage: 7/30GB (25%)
14 | # CPU Load: 0.22
15 |
16 | MEM=$(free -m | awk 'NR==2{printf "%.2f", $3/$2 }') # total mem/used mem (does not look at swap)
17 | CPU=$(top -bn1 | head -n 1 | cut -d "," -f 5 | tr -d " ") # last 5 min CPU load average as "0.03" for 3%
18 | DSKP=$(df -h | awk '$NF=="/"{printf substr($5, 1, length($5)-1)}') # e.g. 25 for 25%
19 | DSK=$(LC_ALL=en_US.UTF-8 printf "%'.2f" "$(echo "scale=10; $DSKP / 100.0" | bc -l)") # e.g. 0.25
20 |
21 | MEMP=$(LC_ALL=en_US.UTF-8 printf "%'.0f" "$(echo "scale=10; $MEM * 100.0" | bc -l)")
22 | CPUP=$(LC_ALL=en_US.UTF-8 printf "%'.0f" "$(echo "scale=10; $CPU * 100.0" | bc -l)")
23 |
24 | CELSIUS=$(cat /sys/class/thermal/thermal_zone0/temp)
25 | TMP=$(echo "scale=0; $CELSIUS/1000" | bc | tr -d "\n")
26 |
27 | # echo "MEM=$MEM, CPU=$CPU, DSK=$DSK, TEMP=$TMP"
28 | # echo "MEM=${MEMP}%, CPU=${CPUP}%, DSK=${DSKP}%, TEMP=${TMP}C"
29 |
30 | RET=0
31 | MEM_STR=""
32 | CPU_STR=""
33 | DSK_STR=""
34 | TMP_STR=""
35 |
36 | if (($(echo "$MEM > $MEM_ALERT" | bc -l))); then
37 | MEM_STR="***$(date +%F\ %R)*** ALERT: memory consumption too high!\n"
38 | RET=$((RET + 1))
39 | fi
40 | if (($(echo "$CPU > $CPU_ALERT" | bc -l))); then
41 | CPU_STR="***$(date +%F\ %R)*** ALERT: CPU usage too high!\n"
42 | RET=$((RET + 2))
43 | fi
44 | if (($(echo "$DSK > $DSK_ALERT" | bc -l))); then
45 | DSK_STR="***$(date +%F\ %R)*** ALERT: disk too full!\n"
46 | RET=$((RET + 4))
47 | fi
48 | if (($(echo "$TMP > $TMP_ALERT" | bc -l))); then
49 | TMP_STR="***$(date +%F\ %R)*** ALERT: CPU temperature too high!\n"
50 | RET=$((RET + 8))
51 | fi
52 |
53 | if [ "$RET" != "0" ]; then
54 | echo "***$(date +%F\ %R)*** ### ALERT ### ALERT ### ALERT ###" >&2 # write this to stderr
55 | echo -e "${MEM_STR}${CPU_STR}${DSK_STR}${TMP_STR}MEM=${MEMP}%, CPU=${CPUP}%, DSK=${DSKP}%, TEMP=${TMP}C\n"
56 | # if there is an alert, also print the top 5 processes, see "top" script, code taken from there
57 | echo "Top 5 CPU consumers:"
58 | #ps -eo %cpu,pid,ppid,cmd --sort=-%cpu | head
59 | ps -eo %cpu,cmd --sort=-%cpu --cols 40 | head -n 5
60 | echo ""
61 | echo "Top 5 RAM consumers:"
62 | #ps -eo %mem,pid,ppid,cmd --sort=-%mem | head
63 | ps -eo %mem,cmd --sort=-%mem --cols 40 | head -n 5
64 | fi
65 |
66 | exit 0
67 |
68 | # EOF
69 |
--------------------------------------------------------------------------------
/eno/scripts/backup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 |
4 | # mybackup must be installed
5 | type mybackup.sh >/dev/null 2>&1 || {
6 | # it was not found in normal path, lets see if we can amplify the PATH by sourcing profile files
7 | . $HOME/.bash_profile 2> /dev/null
8 | . $HOME/.profile 2> /dev/null
9 | type mybackup.sh >/dev/null 2>&1 || {
10 | echo "This script requires that you install the script \"mybackup.sh\" on the server."
11 | exit 0
12 | }
13 | }
14 |
15 | # echo "***$(date +%F\ %R)*** BACKUP: ***"
16 | mybackup.sh "--terse"
17 |
18 | # EOF
19 |
--------------------------------------------------------------------------------
/eno/scripts/btc.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # use TORIFY or TORPROXY, but do NOT use BOTH of them!
4 | # TORIFY="torify"
5 | TORIFY="" # replaced with TORPROXY, don't use both
6 | TORPROXY=" --socks5-hostname localhost:9050 "
7 |
8 | # tor and torify should be installed for your privacy.
9 | type torify > /dev/null 2>&1 || {
10 | echo "It is recommended that you install the packge \"tor\" on the server for privacy."
11 | TORIFY=""
12 | TORPROXY=""
13 | }
14 |
15 | if [ "$DEBUG" == "1" ] || [ "${DEBUG,,}" == "true" ]; then
16 | DEBUG="true"
17 | fi
18 | if [ $# -gt 1 ]; then
19 | echo "Too many arguments."
20 | exit 1
21 | fi
22 | if [ "$#" == "1" ]; then
23 | case "${1,,}" in
24 | 'notor' | 'notorify' | '--notor' | '--notorify')
25 | [ "${DEBUG}" == "true" ] && echo "Torify and Tor Proxy will be turned off."
26 | TORIFY=""
27 | TORPROXY=""
28 | ;;
29 | *)
30 | echo "Unknown argument $*."
31 | exit 1
32 | ;;
33 | esac
34 | fi
35 |
36 | # # OPTION 1: MESSARI SOURCE
37 | # # disadvantage: does not have EUR price
38 | # # $TORIFY curl --silent --compressed "https://data.messari.io/api/v1/assets/bitcoin/metrics" | jq '.data.market_data |
39 | # # .percent_change_usd_last_24_hours, .real_volume_last_24_hours , .price_usd' 2>&1 # 2>> /dev/null
40 | #
41 | # BASELIST=$($TORIFY curl $TORPROXY --silent --compressed "https://data.messari.io/api/v1/assets/bitcoin/metrics" |
42 | # jq '.data.market_data | keys_unsorted[] as $k | "\($k), \(.[$k] )"' |
43 | # grep -e price_usd -e real_volume_last_24_hours -e percent_change_usd_last_24_hours | tr -d "\"")
44 | # # returns something like
45 | # # price_usd, 9632.330680167783
46 | # # real_volume_last_24_hours, 1418555108.5501404
47 | # # percent_change_usd_last_24_hours, 1.152004386319294
48 | # if [ "$BASELIST" == "" ]; then
49 | # echo "Error: No data available. Are you sure the network is up?"
50 | # if [ "${TORIFY}" != "" ] || [ "${TORPROXY}" != "" ]; then
51 | # echo "Error: Are you sure Tor is running? Start Tor or add 'notor' as argument!"
52 | # fi
53 | # exit 1
54 | # fi
55 | # LC_ALL=en_US.UTF-8 printf "Price: %'.0f USD\n" "$(echo "$BASELIST" | grep price_usd | cut -d "," -f 2)"
56 | # LC_ALL=en_US.UTF-8 printf "Change: %'.1f %%\n" "$(echo "$BASELIST" | grep percent_change_usd_last_24_hours | cut -d "," -f 2)"
57 | # LC_ALL=en_US.UTF-8 printf "Volume: %'.0f USD\n" "$(echo "$BASELIST" | grep real_volume_last_24_hours | cut -d "," -f 2)"
58 |
59 | # OPTION 2: COINDESK SOURCE
60 | # disadvantage: does not have % change
61 | # https://api.coindesk.com/v1/bpi/currentprice.json
62 | # returns:
63 | # { "time":{"updated":"Mar 2, 2021 18:14:00 UTC","updatedISO":"2021-03-02T18:14:00+00:00","updateduk":"Mar 2, 2021 at 18:14 GMT"},
64 | # "disclaimer":"This ...","chartName":"Bitcoin",
65 | # "bpi":{"USD":{"code":"USD","symbol":"$","rate":"48,011.8840","description":"United States Dollar","rate_float":48011.884},...,
66 | # "EUR":{"code":"EUR","symbol":"€","rate":"39,745.6779","description":"Euro","rate_float":39745.6779}}}
67 | BASELIST2=$($TORIFY curl $TORPROXY --silent --compressed "https://api.coindesk.com/v1/bpi/currentprice.json" |
68 | jq '.bpi.EUR.rate_float, .bpi.USD.rate_float')
69 | if [ "$BASELIST2" == "" ]; then
70 | echo "Error: No data available. Are you sure the network is up?"
71 | if [ "${TORIFY}" != "" ] || [ "${TORPROXY}" != "" ]; then
72 | echo "Error: Are you sure Tor is running? Start Tor or add 'notor' as argument!"
73 | fi
74 | exit 1
75 | fi
76 |
77 | LC_ALL=en_US.UTF-8 dm1=$(date +%F --date='yesterday')
78 | LC_ALL=en_US.UTF-8 dm2=$(date +%F --date='2 days ago')
79 | LC_ALL=en_US.UTF-8 dm3=$(date +%F --date='3 days ago')
80 | # {"bpi":{"2013-09-01":128.2597,"2013-09-02":127.3648,"2013-09-03":127.5915,"2013-09-04":120.5738,"2013-09-05":120.5333},
81 | # "disclaimer":"This data ...","time":{"updated":"Sep 6, 2013 00:03:00 UTC","updatedISO":"2013-09-06T00:03:00+00:00"}}
82 | # shellcheck disable=SC2089
83 | jqargs=".bpi.\"${dm1}\",.bpi.\"${dm2}\",.bpi.\"${dm3}\""
84 | BASELIST3=$($TORIFY curl $TORPROXY --silent --compressed "https://api.coindesk.com/v1/bpi/historical/close.json?start=${dm3}&end=${dm1}" |
85 | jq "$jqargs")
86 | price_dm0="$(echo "$BASELIST2" | tail -n 1)" # price USD today
87 | price_em0="$(echo "$BASELIST2" | head -n 1)" # price EUR today
88 | price_dm1="$(echo "$BASELIST3" | head -n 1)" # price USD yesterday
89 | price_dm2="$(echo "$BASELIST3" | head -n 2 | tail -n 1)" # price USD 2 days ago
90 | price_dm3="$(echo "$BASELIST3" | tail -n 1)" # price USD 3 days ago
91 | change_dm1=$(echo "scale=4; ($price_dm0-$price_dm1)/$price_dm1*100" | bc) # today's change compared to yesterday
92 | change_dm2=$(echo "scale=4; ($price_dm0-$price_dm2)/$price_dm2*100" | bc) # today's change compared to yesterday
93 | change_dm3=$(echo "scale=4; ($price_dm0-$price_dm3)/$price_dm3*100" | bc) # today's change compared to yesterday
94 | chartdown_emoji=📉
95 | chartup_emoji=📈
96 | dollar_emoji=💵
97 | euro_emoji=💶
98 | [[ $change_dm1 == -* ]] && chart1=$chartdown_emoji || chart1=$chartup_emoji
99 | [[ $change_dm2 == -* ]] && chart2=$chartdown_emoji || chart2=$chartup_emoji
100 | [[ $change_dm3 == -* ]] && chart3=$chartdown_emoji || chart3=$chartup_emoji
101 | # price, and trend in comparison to yesterday close
102 | LC_ALL=en_US.UTF-8 printf "Price: %'.0f EUR € %s %s\n" "$price_em0" "$euro_emoji" "$chart1"
103 | LC_ALL=en_US.UTF-8 printf "Price: %'.0f USD \$ %s %s\n" "$price_dm0" "$dollar_emoji" "$chart1"
104 | # today's price in satoshis per dollar (euro), today $1 gets you so many satoshis
105 | LC_ALL=en_US.UTF-8 printf "Price: €1 gets you %'.0f sats %s\n" "$(echo "scale=4; 100000000/$price_em0" | bc)" "$euro_emoji"
106 | LC_ALL=en_US.UTF-8 printf "Price: \$1 gets you %'.0f sats %s\n" "$(echo "scale=4; 100000000/$price_dm0" | bc)" "$dollar_emoji"
107 | # todays price compared to the price from last days, trend, if positiv then today's priceis higher, i.e. the price has gone up.
108 | LC_ALL=en_US.UTF-8 printf "Price: %'.0f USD (yesterday) %+'5.1f %% %s \n" "$(echo "$BASELIST3" | head -n 1)" "$change_dm1" "$chart1"
109 | LC_ALL=en_US.UTF-8 printf "Price: %'.0f USD (2 days ago) %+'5.1f %% %s \n" "$(echo "$BASELIST3" | head -n 2 | tail -n 1)" "$change_dm2" "$chart2"
110 | LC_ALL=en_US.UTF-8 printf "Price: %'.0f USD (3 days ago) %+'5.1f %% %s \n" "$(echo "$BASELIST3" | tail -n 1)" "$change_dm3" "$chart3"
111 |
112 | # EOF
113 |
--------------------------------------------------------------------------------
/eno/scripts/check.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # e.g. check os
4 |
5 | if [ "$#" == "0" ]; then
6 | echo "You must be checking the health of something. Try \"check os\"."
7 | echo "\"bot\", \"matrix\", \"os\", \"services\", and \"world\" are configured on server."
8 | exit 0
9 | fi
10 | arg1=$1
11 | case "${arg1,,}" in
12 | "bot" | "eno" | "mybot")
13 | echo "The bot will check the health of itself."
14 | systemctl status matrix-eno-bot
15 | ;;
16 | "matrix")
17 | echo "The bot will check the health of matrix service."
18 | # the name of the service might vary based on installation from : synapse-matrix, matrix, etc.
19 | # let#s check all services that contain "matrix" in their name
20 | service --status-all | grep -i matrix | tr -s " " | cut -d " " -f 5 | while read -r p; do
21 | systemctl status "$p"
22 | done
23 | ;;
24 | "os")
25 | echo "The bot will check for possible updates for the operating system"
26 | type apt >/dev/null 2>&1 && type dnf >/dev/null 2>&1 && echo "Don't know how to check for updates as your system does neither support apt nor dnf." && exit 0
27 | # dnf OR apt exists
28 | type dnf >/dev/null 2>&1 || {
29 | apt list --upgradeable
30 | apt-get --just-print upgrade 2>/dev/null | grep -v "NOTE: This is only a simulation!" |
31 | grep -v "apt-get needs root privileges for real execution." |
32 | grep -v "Keep also in mind that locking is deactivated," |
33 | grep -v "so don't depend on the relevance to the real current situation!"
34 | }
35 | type apt >/dev/null 2>&1 || {
36 | dnf -q check-update
37 | dnf -q updateinfo
38 | dnf -q updateinfo list sec
39 | }
40 | ;;
41 | "services" | "service")
42 | service --status-all
43 | ;;
44 | "world")
45 | echo "The world definitely needs some upgrades!"
46 | ;;
47 | *)
48 | echo "This bot does not know how to check up on ${arg1}."
49 | echo "Only \"bot\", \"matrix\", \"os\", \"services\", and \"world\" are configured on server."
50 | ;;
51 | esac
52 |
53 | # EOF
54 |
--------------------------------------------------------------------------------
/eno/scripts/config.rc.example:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # If desired, fill out the fields and rename file to "config.rc".
4 | # File must be located in same directory as Eno bash scripts.
5 | # Using this file is optional. It helps you separate some
6 | # private values from the rest of the bash scripts for better security.
7 |
8 | ###############################################################################
9 | # CONFIG FILE FOR PRIVATE RESOURCES THAT NEED TO BE PROTECTED
10 | # DO NOT PUBLISH THIS FILE
11 | ###############################################################################
12 |
13 | # START-SECRET ==============================================================
14 | export RESTART_DEFAULT_PASSWD_HASH="hash-of-your-password"
15 | # END-SECRET ==============================================================
16 | export TIDES_DEFAULT_CITY1="SomeCity"
17 | export TIDES_DEFAULT_CITY2="SomeCityShortName"
18 | export TIDES_DEFAULT_CITY3="SomeCityLongName"
19 | export WAVES_DEFAULT_CITY1="SomeCity-Surf-Report/SomeNumber/" # see MagicSeaWeed.com
20 | export WAVES_DEFAULT_CITY2="SomeCityShortName"
21 | export WAVES_DEFAULT_CITY3="SomeCityLongName"
22 | export WEATHER_DEFAULT_CITY1="SomeCityId" # see ansiweather
23 | export WEATHER_DEFAULT_CITY2="SomeCityShortName"
24 | export WEATHER_DEFAULT_CITY3="SomeCityLongName"
25 | # START-SECRET ==============================================================
26 | export USERS_MYACCESSTOKENWITHADMINPERMISSIONS="some-very-long-access-token-from-Matrix-account-with-admin-permissions"
27 | export USERS_MYHOMESERVER="https://matrix.example.com"
28 | # END-SECRET ==============================================================
29 | # START-SECRET ==============================================================
30 | export WAKE_PASSWD_HASH="hash-of-some-password"
31 | export WAKE_MAC1="So:me:Ma:cA:dd:re"
32 | export WAKE_NICK1A="SomeHostnameShort"
33 | export WAKE_NICK1B="SomeHostnameLong"
34 | # END-SECRET ==============================================================
35 |
--------------------------------------------------------------------------------
/eno/scripts/cputemp.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # single Core
4 | # CELSIUS=$(cat /sys/class/thermal/thermal_zone0/temp) && echo "scale=2; $CELSIUS/1000" | bc | tr "\n" " " && echo "Celsius"
5 |
6 | # multi-core CPU
7 | cat /sys/class/thermal/thermal_zone*/temp 2>/dev/null | while read -r p; do
8 | # echo "$p"
9 | CELSIUS="$p" && echo "scale=2; $CELSIUS/1000" | bc | tr "\n" " " && echo "Celsius"
10 | done
11 | if [ "$(cat /sys/class/thermal/thermal_zone*/temp 2>/dev/null | wc -l)" == "0" ]; then
12 | echo "No CPU temperature information found."
13 | fi
14 | exit 0
15 |
16 | # EOF
17 |
--------------------------------------------------------------------------------
/eno/scripts/datetime.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo -n "Server time: "
4 | date
5 | echo -n "Los Angeles: "
6 | TZ='America/Los_Angeles' date
7 | echo -n "Paris/Madrid: "
8 | TZ='Europe/Madrid' date
9 | echo -n "Lima: "
10 | TZ='America/Lima' date
11 | echo -n "Melbourne: "
12 | TZ='Australia/Melbourne' date
13 |
14 | # EOF
15 |
--------------------------------------------------------------------------------
/eno/scripts/ddg.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # ddgr must be installed
4 | type ddgr >/dev/null 2>&1 || {
5 | echo "For duckduckgo search to work you must first install the packge \"ddgr\" on the server."
6 | exit 0
7 | }
8 |
9 | if [ "$#" == "0" ]; then
10 | echo "You must be looking for something. Try \"ddg matrix news\"."
11 | exit 0
12 | fi
13 |
14 | ddgr --np -n=10 -C "$@"
15 |
16 | exit 0
17 |
18 | # EOF
19 |
--------------------------------------------------------------------------------
/eno/scripts/disks.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | du -hs "$HOME" 2>>/dev/null
4 | echo "========================="
5 | df -h 2>>/dev/null
6 |
7 | # EOF
8 |
--------------------------------------------------------------------------------
/eno/scripts/echo.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | """Echo back the command's arguments."""
4 |
5 | import sys
6 |
7 | if __name__ == "__main__":
8 | response = " ".join(sys.argv[1:])
9 | if response.strip() == "":
10 | response = "echo!"
11 |
12 | print(response)
13 |
--------------------------------------------------------------------------------
/eno/scripts/eth.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # use TORIFY or TORPROXY, but do NOT use BOTH of them!
4 | # TORIFY="torify"
5 | TORIFY="" # replaced with TORPROXY, dont use both
6 | TORPROXY=" --socks5-hostname localhost:9050 "
7 |
8 | # tor and torify should be installed for your privacy.
9 | type torify >/dev/null 2>&1 || {
10 | echo "It is recommended that you install the packge \"tor\" on the server for privacy."
11 | TORIFY=""
12 | TORPROXY=""
13 | }
14 |
15 | # $TORIFY curl --silent --compressed "https://data.messari.io/api/v1/assets/ethereum/metrics" | jq '.data.market_data' 2>> /dev/null
16 | # $TORIFY curl --silent --compressed "https://data.messari.io/api/v1/assets/ethereum/metrics" |
17 | # jq '.data.market_data | keys_unsorted[] as $k | "\($k), \(.[$k] )"' |
18 | # grep -e price_usd -e real_volume_last_24_hours -e percent_change_usd_last_24_hours -e price_btc | tr -d "\"" | rev | cut -c9- | rev | tr "," ":" 2>&1
19 |
20 | BASELIST=$($TORIFY curl $TORPROXY --silent --compressed "https://data.messari.io/api/v1/assets/ethereum/metrics" |
21 | jq '.data.market_data | keys_unsorted[] as $k | "\($k), \(.[$k] )"' |
22 | grep -e price_usd -e real_volume_last_24_hours -e percent_change_usd_last_24_hours -e price_btc | tr -d "\"")
23 | # returns something like
24 | # price_usd, 9632.330680167783
25 | # real_volume_last_24_hours, 1418555108.5501404
26 | # percent_change_usd_last_24_hours, 1.152004386319294
27 | LC_ALL=en_US.UTF-8 printf "Price: %'.0f USD\n" "$(echo "$BASELIST" | grep price_usd | cut -d "," -f 2)"
28 | LC_ALL=en_US.UTF-8 printf "Price: %'.4f BTC\n" "$(echo "$BASELIST" | grep price_btc | cut -d "," -f 2)"
29 | LC_ALL=en_US.UTF-8 printf "Change: %'.1f %%\n" "$(echo "$BASELIST" | grep percent_change_usd_last_24_hours | cut -d "," -f 2)"
30 | LC_ALL=en_US.UTF-8 printf "Volume: %'.0f USD\n" "$(echo "$BASELIST" | grep real_volume_last_24_hours | cut -d "," -f 2)"
31 |
32 | # EOF
33 |
--------------------------------------------------------------------------------
/eno/scripts/firewall.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # ufw must be installed
4 | type ufw >/dev/null 2>&1 || {
5 | echo "This script requires that you install the packge \"ufw\" on the server to check your firewall."
6 | exit 0
7 | }
8 |
9 | fi=$(sudo ufw status verbose)
10 | bold=$(tput bold)
11 | red=$(tput setaf 1)
12 | yellow=$(tput setaf 3)
13 | green=$(tput setaf 2)
14 | reset=$(tput sgr0)
15 | fi=${fi//deny/${green}${bold}deny${reset}}
16 | fi=${fi//reject/${yellow}${bold}reject${reset}}
17 | fi=${fi//allow/${red}${bold}allow${reset}}
18 | fi=${fi//disabled/${green}${bold}disabled${reset}}
19 | fi=${fi//enabled/${red}${bold}enabled${reset}}
20 | echo -e -n "$fi"
21 | echo
22 |
23 | # EOF
24 |
--------------------------------------------------------------------------------
/eno/scripts/hello.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | if [[ "$ENO_SENDER" != "" ]]; then
3 | PREFIX="${ENO_SENDER}, "
4 | else
5 | PREFIX=""
6 | fi
7 |
8 | MSG=$(echo "You're an awesome friend.
9 | You're a gift to those around you.
10 | You're a smart cookie.
11 | You are awesome!
12 | You have impeccable manners.
13 | I like your style.
14 | You have the best laugh.
15 | I appreciate you.
16 | You are the most perfect you there is.
17 | You are enough.
18 | You're strong.
19 | Your perspective is refreshing.
20 | I'm grateful to know you.
21 | You light up the room.
22 | You deserve a hug right now.
23 | You should be proud of yourself.
24 | You're more helpful than you realize.
25 | You have a great sense of humor.
26 | You've got an awesome sense of humor!
27 | You are really courageous.
28 | Your kindness is a balm to all who encounter it.
29 | You're all that and a super-size bag of chips.
30 | On a scale from 1 to 10, you're an 11.
31 | You are strong.
32 | You're even more beautiful on the inside than you are on the outside.
33 | You have the courage of your convictions.
34 | I'm inspired by you.
35 | You're like a ray of sunshine on a really dreary day.
36 | You are making a difference.
37 | Thank you for being there for me.
38 | You bring out the best in other people.
39 | Your ability to recall random factoids at just the right time is impressive.
40 | You're a great listener.
41 | How is it that you always look great, even in sweatpants?
42 | Everything would be better if more people were like you!
43 | I bet you sweat glitter.
44 | You were cool way before hipsters were cool.
45 | That color is perfect on you.
46 | Hanging out with you is always a blast.
47 | You always know -- and say -- exactly what I need to hear when I need to hear it.
48 | You help me feel more joy in life.
49 | You may dance like no one is watching, but everyone is watching because you are an amazing dancer!
50 | Being around you makes everything better!
51 | When you say, \"I meant to do that,\" I totally believe you.
52 | When you're not afraid to be yourself is when you are most incredible.
53 | Colors seem brighter when you're around.
54 | You're more fun than a ball pit filled with candy. (And seriously, what could be more fun than that?)
55 | That thing you don't like about yourself is what makes you so interesting.
56 | You're wonderful.
57 | You have cute elbows. For reals!
58 | Jokes are funnier when you tell them.
59 | You're better than a triple-scoop ice cream cone. With sprinkles.
60 | When I'm down you always say something encouraging to help me feel better.
61 | You are really kind to people around you.
62 | You're one of a kind!
63 | You help me be the best version of myself.
64 | If you were a box of crayons, you'd be the giant name-brand one with the built-in sharpener.
65 | You should be thanked more often. So thank you!!
66 | Our community is better because you're in it.
67 | Someone is getting through something hard right now because you've got their back.
68 | You have the best ideas.
69 | You always find something special in the most ordinary things.
70 | Everyone gets knocked down sometimes, but you always get back up and keep going.
71 | You're a candle in the darkness.
72 | You're a great example to others.
73 | Being around you is like being on a happy little vacation.
74 | You always know just what to say.
75 | You're always learning new things and trying to better yourself, which is awesome.
76 | If someone based an Internet meme on you, it would have impeccable grammar.
77 | You could survive a Zombie apocalypse.
78 | You're more fun than bubble wrap.
79 | When you make a mistake, you try to fix it.
80 | You're great at figuring stuff out.
81 | Your voice is magnificent.
82 | The people you love are lucky to have you in their lives.
83 | You're like a breath of fresh air.
84 | You make my insides jump around in the best way.
85 | You're so thoughtful.
86 | Your creative potential seems limitless.
87 | Your name suits you to a T.
88 | Your quirks are so you -- and I love that.
89 | When you say you will do something, I trust you.
90 | Somehow you make time stop and fly at the same time.
91 | When you make up your mind about something, nothing stands in your way.
92 | You seem to really know who you are.
93 | Any team would be lucky to have you on it.
94 | In high school I bet you were voted \"most likely to keep being awesome.\"
95 | I bet you do the crossword puzzle in ink.
96 | Babies and small animals probably love you.
97 | If you were a scented candle they'd call it Perfectly Imperfect (and it would smell like summer).
98 | There is ordinary, and then there's you.
99 | You are someone's reason to smile.
100 | You are even better than a unicorn, because you're real.
101 | How do you keep being so funny and making everyone laugh?
102 | You have a good head on your shoulders.
103 | Has anyone ever told you that you have great posture?
104 | The way you treasure your loved ones is incredible.
105 | You're really something special.
106 | Thank you for being you.
107 | You are a joy.
108 | You are a wonderful part of our family.
109 | You are excellent.
110 | You are so good at this.
111 | You are such a blessing to me.
112 | You are such a leader at school.
113 | You are worth so much to me.
114 | You brighten my life.
115 | You can do anything you put your mind to.
116 | You color my world.
117 | You do things with excellence.
118 | You encourage me.
119 | You have incredible insight.
120 | You have my heart.
121 | You impact me every day.
122 | You love me well.
123 | You love your friends well.
124 | You make a difference.
125 | You make gray skies disappear.
126 | You make me smile.
127 | You make memories sweeter.
128 | You make my days sweeter.
129 | You matter to me.
130 | You put others first.
131 | You rock.
132 | You set a great example.
133 | You shine every day.
134 | You’re a great chatter. I am a fan of yours.
135 | You’re a great leader.
136 | You’re a great bot user.
137 | You’re a team player.
138 | You’re amazing.
139 | You’re artistic.
140 | You’re athletic.
141 | You’re awesome.
142 | You’re beautiful.
143 | You’re compassionate.
144 | You’re creative.
145 | You’re delightful.
146 | You’re doing great things.
147 | You’re fantastic.
148 | You’re fun.
149 | You’re handsome.
150 | You’re helpful.
151 | You’re incredible.
152 | You’re inspiring.
153 | You’re kind.
154 | You’re marvelous.
155 | You’re nice to others.
156 | You’re one of a kind.
157 | You’re outstanding.
158 | You’re positive.
159 | You’re radiant.
160 | You’re smart.
161 | You’re so fun to play with.
162 | You’re so fun-loving.
163 | You’re so hopeful.
164 | You’re so refreshing.
165 | You’re so respectful.
166 | You’re so special.
167 | You’re so strong.
168 | You’re so trustworthy.
169 | You’re the best.
170 | You’re the light of my life.
171 | You’re thoughtful.
172 | You’re tremendous.
173 | You’re unbelievable.
174 | You’re unique.
175 | You have great dreams.
176 | You have great ideas.
177 | You make me so proud.
178 | You win me over every day.
179 | You’re intelligent.
180 | You’re interesting.
181 | You’re talented.
182 | I’m so glad you’re mine." | shuf -n1)
183 |
184 | echo "${PREFIX}$MSG"
185 |
186 | # EOF
187 |
--------------------------------------------------------------------------------
/eno/scripts/hn.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | TORIFY="torify"
4 |
5 | # tor and torify should be installed for your privacy.
6 | type torify >/dev/null 2>&1 || {
7 | echo "It is recommended that you install the packge \"tor\" on the server for privacy."
8 | TORIFY=""
9 | }
10 |
11 | function gethackernewsfromweb() {
12 | $TORIFY w3m -dump news.ycombinator.com 2>&1 | grep -v -E 'points.*comments' |
13 | grep -v -E ' points by .* minutes ago | hide | discuss' |
14 | grep -v -E ' ago | hide' |
15 | grep -v 'Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet' |
16 | grep -v "Legal | Apply to YC | Contact" |
17 | grep -v -E ' *Search: \[ *\]$' |
18 | grep -v " submit" |
19 | grep -v " More" |
20 | grep -v -E ' \*$' | sed '/^[[:space:]]*$/d' | sed 's/^......//'
21 | # remove lines with only whitespaces, remove leading 6 characters
22 | }
23 |
24 | HNSH="$HOME/Scripts/hn.sh"
25 | # hn script installed?
26 | # If hn.sh script is not installed, no problem, we get the data from the web.
27 | type "$HNSH" >/dev/null 2>&1 || {
28 | echo "Getting Hacker News from web: "
29 | gethackernewsfromweb
30 | exit 0
31 | }
32 |
33 | if [ "$#" == "0" ]; then
34 | gethackernewsfromweb
35 | exit 0
36 | fi
37 | case "$1" in
38 | '' | *[!0-9]*)
39 | echo "First argument is not a number. Skipping. Try \"hn\" or \"hn 5\"."
40 | exit 0
41 | ;;
42 | *)
43 | # echo "First argument is a number. "
44 | ;;
45 | esac
46 | $HNSH "$1" | grep -v -e "points" -e "comments" | grep -v lurker | grep -v Initializing | grep -v "Fetching posts"
47 | exit 0
48 |
49 | # EOF
50 |
--------------------------------------------------------------------------------
/eno/scripts/mn.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # mn.sh # returns 1 news article (the last)
4 | # mn.sh 1 # returns 1 news article (the last)
5 | # mn.sh 2 # returns 2 news articles (the last 2)
6 | # mn.sh 15 # returns 15 news articles (the last 15)
7 | # mn.sh y # return the news articles from yesterday (could be zero, one or more)
8 | # mn.sh yesterday # return the news articles from yesterday (could be zero, one or more)
9 | # mn.sh 2020-10-22 # return the news articles from a specific day, only this day (not "since this day")
10 |
11 | # DEBUG="true"
12 |
13 | # use TORIFY or TORPROXY, but do NOT use BOTH of them!
14 | # TORIFY="torify"
15 | TORIFY="" # replaced with TORPROXY, dont use both
16 | TORPROXY=" --socks5-hostname localhost:9050 "
17 |
18 | # tor and torify should be installed for your privacy.
19 | type torify >/dev/null 2>&1 || {
20 | echo "It is recommended that you install the packge \"tor\" on the server for privacy."
21 | TORIFY=""
22 | TORPROXY=""
23 | }
24 |
25 | if [ "$DEBUG" == "1" ]; then
26 | DEBUG="true"
27 | fi
28 |
29 | if [ "$#" == "0" ]; then
30 | arg1=1 # default value if none given by user
31 | fi
32 | if [ "$#" == "1" ] || [ "$#" == "2" ]; then
33 | case "${1,,}" in
34 | 'y' | 'yesterday')
35 | ydate=$(date +%F --date='yesterday')
36 | [ "${DEBUG,,}" == "true" ] && echo "Getting yesterday's news for day \"$ydate\"."
37 | arg1='yesterday'
38 | ;;
39 | 202[0-9]-[0-1][0-9]-[0-3][0-9])
40 | ydate=$1
41 | [ "${DEBUG,,}" == "true" ] && echo "Getting news for day \"$ydate\"."
42 | arg1=$1
43 | ;;
44 | 'notor' | 'notorify' | '--notor' | '--notorify')
45 | echo "Torify and Tor Proxy have been turned off."
46 | TORIFY=""
47 | TORPROXY=""
48 | ;;
49 | '' | *[!0-9]*)
50 | echo "First argument is not a number. Skipping. Try \"mn\" or \"mn 2\"."
51 | exit 1
52 | ;;
53 | *)
54 | # echo "First argument is a number. "
55 | arg1=$1
56 | ;;
57 | esac
58 | fi
59 | if [ "$#" == "2" ]; then
60 | case "${2,,}" in
61 | 'notor' | 'notorify' | '--notor' | '--notorify')
62 | echo "Torify and Tor Proxy have been turned off."
63 | TORIFY=""
64 | TORPROXY=""
65 | ;;
66 | *)
67 | echo "Invalid second argument. Try \"mn\", \"mn 2\", or \"mn 2 notor\"."
68 | exit 2
69 | ;;
70 | esac
71 | fi
72 | if [ $# -gt 2 ]; then
73 | echo "Too many arguments. Expected at most 2, but found $#. Try \"mn\", \"mn 2\", or \"mn 2 notor\"."
74 | exit 3
75 | fi
76 |
77 | function dojson() {
78 | [ "${DEBUG,,}" == "true" ] && echo "DEBUG: You requested \"${arg1,,}\" news article"
79 | case "${arg1,,}" in
80 | 'y' | 'yesterday' | 202[0-9]-[0-1][0-9]-[0-3][0-9])
81 | x=0
82 | while :; do
83 | mndate=$(echo "$mnjson" | jq ".data[$x].published_at")
84 | # echo "MN article $x is from date $mndate."
85 | mndate=${mndate:1:10} # is of form "2020-08-31" INCLUDING quotes!
86 | # echo "MN article $x is from date $mndate and comparing it to date $ydate."
87 | if [[ "$mndate" < "$ydate" ]]; then
88 | # echo "mn date $mndate < ydate $ydate. Exiting"
89 | break # jump out of infinite loop
90 | fi
91 | if [ "$mndate" == "$ydate" ]; then
92 | echo "$mnjson" | jq ".data[$x] | .url, .title , .content " | sed 's/\\n/\'$'\n''/g' | sed 's/\!\[\]//g' 2>>/dev/null
93 | fi
94 | x=$(($x + 1))
95 | if [[ "$mndate" > "$ydate" ]]; then
96 | continue # go to next element
97 | fi
98 | done
99 | ;;
100 | '' | 1)
101 | [ "${DEBUG,,}" == "true" ] && echo "You requested 1 news article"
102 | echo "$mnjson" | jq '.data[0] | .url, .title , .content ' | sed 's/\\n/\'$'\n''/g' | sed 's/\!\[\]//g' 2>>/dev/null
103 | ;;
104 | 2)
105 | [ "${DEBUG,,}" == "true" ] && echo "You requested ${arg1,,} news articles"
106 | echo "$mnjson" | jq '.data[0,1] | .url, .title , .content ' | sed '0~3 s/$/\n\n/g' 2>>/dev/null
107 | ;;
108 | 3)
109 | [ "${DEBUG,,}" == "true" ] && echo "You requested ${arg1,,} news articles"
110 | echo "$mnjson" | jq '.data[0,1,2] | .url, .title , .content ' | sed '0~3 s/$/\n\n/g' 2>>/dev/null
111 | ;;
112 | 4)
113 | [ "${DEBUG,,}" == "true" ] && echo "You requested ${arg1,,} news articles"
114 | echo "$mnjson" | jq '.data[0,1,2,3] | .url, .title , .content ' | sed '0~3 s/$/\n\n/g' 2>>/dev/null
115 | ;;
116 | 5)
117 | [ "${DEBUG,,}" == "true" ] && echo "You requested ${arg1,,} news articles"
118 | echo "$mnjson" | jq '.data[0,1,2,3,4] | .url, .title , .content ' | sed '0~3 s/$/\n\n/g' 2>>/dev/null
119 | ;;
120 | 6)
121 | [ "${DEBUG,,}" == "true" ] && echo "You requested ${arg1,,} news articles"
122 | echo "$mnjson" | jq '.data[0,1,2,3,4,5] | .url, .title , .content ' | sed '0~3 s/$/\n\n/g' 2>>/dev/null
123 | ;;
124 | 7)
125 | [ "${DEBUG,,}" == "true" ] && echo "You requested ${arg1,,} news articles"
126 | echo "$mnjson" | jq '.data[0,1,2,3,4,5,6] | .url, .title , .content ' | sed '0~3 s/$/\n\n/g' 2>>/dev/null
127 | ;;
128 | 8)
129 | [ "${DEBUG,,}" == "true" ] && echo "You requested ${arg1,,} news articles"
130 | echo "$mnjson" | jq '.data[0,1,2,3,4,5,6,7] | .url, .title , .content ' | sed '0~3 s/$/\n\n/g' 2>>/dev/null
131 | ;;
132 | 9)
133 | [ "${DEBUG,,}" == "true" ] && echo "You requested ${arg1,,} news articles"
134 | echo "$mnjson" | jq '.data[0,1,2,3,4,5,6,7,8] | .url, .title , .content ' | sed '0~3 s/$/\n\n/g' 2>>/dev/null
135 | ;;
136 | 10)
137 | [ "${DEBUG,,}" == "true" ] && echo "You requested ${arg1,,} news articles"
138 | echo "$mnjson" | jq '.data[0,1,2,3,4,5,6,7,8,9] | .url, .title , .content ' | sed '0~3 s/$/\n\n/g' 2>>/dev/null
139 | ;;
140 | 11)
141 | [ "${DEBUG,,}" == "true" ] && echo "You requested ${arg1,,} news articles"
142 | echo "$mnjson" | jq '.data[0,1,2,3,4,5,6,7,8,9,10] | .url, .title , .content ' | sed '0~3 s/$/\n\n/g' 2>>/dev/null
143 | ;;
144 | 12)
145 | [ "${DEBUG,,}" == "true" ] && echo "You requested ${arg1,,} news articles"
146 | echo "$mnjson" | jq '.data[0,1,2,3,4,5,6,7,8,9,10,11] | .url, .title , .content ' | sed '0~3 s/$/\n\n/g' 2>>/dev/null
147 | ;;
148 | 13)
149 | [ "${DEBUG,,}" == "true" ] && echo "You requested ${arg1,,} news articles"
150 | echo "$mnjson" | jq '.data[0,1,2,3,4,5,6,7,8,9,10,11,12] | .url, .title , .content ' | sed '0~3 s/$/\n\n/g' 2>>/dev/null
151 | ;;
152 | 14)
153 | [ "${DEBUG,,}" == "true" ] && echo "You requested ${arg1,,} news articles"
154 | echo "$mnjson" | jq '.data[0,1,2,3,4,5,6,7,8,9,10,11,12,13] | .url, .title , .content ' | sed '0~3 s/$/\n\n/g' 2>>/dev/null
155 | ;;
156 | 15)
157 | [ "${DEBUG,,}" == "true" ] && echo "You requested ${arg1,,} news articles"
158 | echo "$mnjson" | jq '.data[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14] | .url, .title , .content ' | sed '0~3 s/$/\n\n/g' 2>>/dev/null
159 | ;;
160 | *)
161 | echo "You want too many articles or you want something foolish. You asked for \"${arg1,,}\". I will give you 1 news article"
162 | echo "$mnjson" | jq '.data[0] | .url, .title , .content ' 2>>/dev/null
163 | ;;
164 | esac
165 | } # dojson() function
166 |
167 | # https://messari.io/api/docs#tag/News
168 | mnjson=$($TORIFY curl $TORPROXY --silent --compressed "https://data.messari.io/api/v1/news?fields=title,content,url,published_at&as-markdown&page=1")
169 | # TESTDATA mnjson="