├── .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 | ![matrix-eno-bot icon](logos/eno-logo.svg) 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 | ![matrix-eno-bot icon](eno-logo.svg) 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 | 19 | 21 | 29 | 35 | 36 | 44 | 50 | 51 | 59 | 65 | 66 | 74 | 80 | 81 | 89 | 95 | 96 | 104 | 110 | 111 | 119 | 125 | 126 | 127 | 152 | 154 | 155 | 157 | image/svg+xml 158 | 160 | 161 | 162 | 163 | 164 | 169 | 177 | 180 | 185 | 193 | 201 | 202 | 203 | 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 | ![matrix-eno-bot icon](../logos/eno-logo.svg) 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="500 Service Error

500 Service Error

" 170 | [ "${DEBUG,,}" == "true" ] && echo -e "DEBUG: REST query returned this data (first 256 bytes): \n${mnjson:0:255}\n" 171 | firstline=$(echo "$mnjson" | head -n 1) 172 | if [[ "${firstline,,}" =~ \.* ]]; then 173 | echo "There was a problem. Messari did not return a JSON object but an HTML page." 174 | echo "$(echo "$mnjson" | grep -i "" -)" 175 | echo "$(echo "$mnjson" | grep -i "<h1>" -)" 176 | exit 4 177 | fi 178 | dojson 179 | 180 | # Asset-based news is the SAME news (subset) of the general news 181 | # echo -e "\n\n\nBTC news:" 182 | # mnjson=$($TORIFY curl $TORPROXY --silent --compressed "https://data.messari.io/api/v1/news?assetKey=BTC&fields=title,content,url,published_at&as-markdown&page=1") 183 | # dojson 184 | # 185 | # echo -e "\n\n\nETH news:" 186 | # mnjson=$($TORIFY curl $TORPROXY --silent --compressed "https://data.messari.io/api/v1/news?assetKey=ETH&fields=title,content,url,published_at&as-markdown&page=1") 187 | # dojson 188 | 189 | # EOF 190 | -------------------------------------------------------------------------------- /eno/scripts/motd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -f /etc/motd ]; then 4 | cat /etc/motd 5 | else 6 | echo "There is no message of the day on your system. So, be happpy." 7 | fi 8 | 9 | # EOF 10 | -------------------------------------------------------------------------------- /eno/scripts/platforminfo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Print distribution information to stdout.""" 3 | import platform 4 | import sys 5 | 6 | 7 | def linux_distribution(): 8 | """Get distribution information.""" 9 | try: 10 | return platform.linux_distribution() 11 | except BaseException: 12 | return "N/A" 13 | 14 | 15 | print( 16 | """Python version: %s 17 | linux_distribution: %s 18 | system: %s 19 | machine: %s 20 | platform: %s 21 | uname: %s 22 | version: %s 23 | """ 24 | % ( 25 | sys.version.split("\n"), 26 | linux_distribution(), 27 | platform.system(), 28 | platform.machine(), 29 | platform.platform(), 30 | platform.uname(), 31 | platform.version(), 32 | ) 33 | ) 34 | 35 | # EOF 36 | -------------------------------------------------------------------------------- /eno/scripts/ps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | free -m | awk 'NR==2{printf "Memory Usage: %s/%sMB (%.2f %%)\n", $3,$2,$3*100/$2 }' 4 | df -h | awk '$NF=="/"{printf "Disk Usage: %d/%dGB (%s)\n", $3,$2,$5}' 5 | # top -bn1 | grep load | awk '{printf "CPU Load: %.2f\n", $(NF-2)}' 6 | CPU=$(top -bn1 | head -n 1 | cut -d "," -f 5 | tr -d " ") # last 5 min CPU load average as "0.03" for 3% 7 | CPUP=$(LC_ALL=en_US.UTF-8 printf "%'.0f" "$(echo "scale=10; $CPU * 100.0" | bc -l)") 8 | echo "CPU Load: $CPUP %" 9 | echo -n "CPU Temperature: " 10 | # assume 'ps.sh' and 'cputemp.sh' are in same directory 11 | #$(dirname "${0}")/cputemp.sh 12 | 13 | # assume cputemp.sh is in PATH 14 | cputemp.sh 15 | 16 | # EOF 17 | -------------------------------------------------------------------------------- /eno/scripts/restart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Put your password hash here 4 | PASSWD_HASH="PutYourPasswordHashHere0123456789abcdef0123456789abcdef012345678" 5 | # or put it into the ./config.rc config file as var RESTART_DEFAULT_PASSWD_HASH 6 | 7 | if [ -f "$(dirname "$0")/config.rc" ]; then 8 | [ "$DEBUG" == "1" ] && echo "Sourcing $(dirname "$0")/config.rc" 9 | # shellcheck disable=SC1090 10 | source "$(dirname "$0")/config.rc" # if it exists, optional, not needed, allows to set env variables 11 | PASSWD_HASH="${RESTART_DEFAULT_PASSWD_HASH}" 12 | fi 13 | 14 | if [ "$#" == "0" ]; then 15 | echo "You must be restarting something. Try \"restart bot\" or \"restart matrix\"." 16 | echo "\"bot\", \"matrix\", \"os\", and \"world\" are configured on server." 17 | exit 0 18 | fi 19 | arg1=$1 20 | arg2=$2 21 | 22 | function dorestart() { 23 | arg1="$1" 24 | arg2="$2" 25 | case "${arg1,,}" in 26 | "bot" | "eno") 27 | echo "The bot will reset itself." 28 | # THIS WILL ONLY WORK IF THE ACCOUNT UNDER WHICH THIS IS EXECUTED HAS PERMISSIONS TO DO SUDO! 29 | p="matrix-eno-bot" 30 | sudo systemctl restart "$p" || 31 | { 32 | echo "Error while trying to restart service \"$p\". systemctl restart \"$p\" failed. Maybe due to missing permissions?" 33 | return 0 34 | } 35 | # the following output will go nowhere, nothing will be returned to user 36 | echo "The bot did restart." 37 | systemctl status matrix-eno-bot 38 | ;; 39 | "world") 40 | echo "Reseting world order. Done!" 41 | ;; 42 | "matrix") 43 | echo "The bot will reset Matrix service" 44 | # the name of the service might vary based on installation from : synapse-matrix, matrix, etc. 45 | # let's be reckless and reset all services that contain "matrix" in their name 46 | # THIS WILL ONLY WORK IF THE ACCOUNT UNDER WHICH THIS IS EXECUTED HAS PERMISSIONS TO DO SUDO! 47 | service --status-all | grep -i matrix | tr -s " " | cut -d " " -f 5 | while read -r p; do 48 | sudo systemctl stop "$p" || 49 | { 50 | echo "Error while trying to stop service \"$p\". systemctl stop \"$p\" failed. Maybe due to missing permissions?" 51 | return 0 52 | } 53 | sleep 1 54 | echo "Service \"$p\" was stopped successfully." 55 | sudo systemctl start "$p" || 56 | { 57 | echo "Error while trying to start service \"$p\". systemctl start \"$p\" failed. Maybe due to missing permissions?" 58 | return 0 59 | } 60 | sleep 1 61 | echo "Service \"$p\" was started successfully." 62 | echo "Status of service \"$p\" is:" 63 | systemctl status "$p" 64 | done 65 | # output will be shown by bot after Matrix restarts and bot reconnects. 66 | ;; 67 | "os") 68 | # in order to reboot OS, one must provide a password 69 | # we compare the hashes here 70 | if [ "$(echo "$arg2" | sha256sum | cut -d ' ' -f 1)" == "$PASSWD_HASH" ]; then 71 | echo "The bot will reboot the server" 72 | sudo systemctl reboot 73 | else 74 | echo "Argument missing or argument wrong. Command ignored due to lack of permissions." 75 | fi 76 | ;; 77 | *) 78 | echo "The bot does not know how to restart \"${arg1}\"." 79 | echo "Only \"bot\", \"matrix\", \"os\", and \"world\" are configured on server." 80 | ;; 81 | esac 82 | } 83 | 84 | dorestart "$arg1" "$arg2" 85 | 86 | exit 0 87 | 88 | # EOF 89 | -------------------------------------------------------------------------------- /eno/scripts/rss.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=" --tor " 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 | # use either rsstail or rssread.py, one or the other 16 | 17 | # A copy of rssread.py can be found in the same repo, same directory, as this rss.sh script. 18 | type rssread.py >/dev/null 2>&1 || { 19 | # it was not found in normal path, lets see if we can amplify the PATH by sourcing profile files 20 | . $HOME/.bash_profile 2>/dev/null 21 | . $HOME/.profile 2>/dev/null 22 | type rssread.py >/dev/null 2>&1 || { 23 | echo "This script requires that you install the script \"rssread.py\" and its dependencies on the server." 24 | exit 0 25 | } 26 | } 27 | 28 | # alternatively rsstail must be installed 29 | # rsstail must be installed 30 | # If you use rsstail, uncomment the following lines 31 | # type rsstail >/dev/null 2>&1 || { 32 | # echo "This script requires that you install the packge \"rsstail\" on the server." 33 | # exit 0 34 | # } 35 | 36 | function readrss() { 37 | if [ "$2" == "" ]; then 38 | echo "Fetching todays's items from feed \"$1\"..." 39 | else 40 | echo "Fetching latest $2 items from feed \"$1\"..." 41 | fi 42 | # if there 3 newlines, it will generate separate posts, but it is nicer and easier to remove later if everything is nicely bundled into 1 post 43 | # so, first we use sed to remove all occurances of 5, 4, and 3 newlines. 44 | # Then we insert 2 newlines after the last newline to create 3 newlines, so that at the end of the feed item the Matrix message is split. 45 | # This way N feed posts always create exactly N Matrix messages. 46 | # Inserting newlines with sed: https://unix.stackexchange.com/questions/429139/replace-newlines-with-sed-a-la-tr 47 | 48 | # shellcheck disable=SC2086 49 | $TORIFY rssread.py $TORPROXY --feed "$1" --number $2 | sed 'H;1h;$!d;x; s/\n\n\n\n\n/\n\n/g' | sed 'H;1h;$!d;x; s/\n\n\n\n/\n\n/g' | 50 | sed 'H;1h;$!d;x; s/\n\n\n/\n\n/g' | sed '/Pub.date: /a \\n\n' # add newlines for separation after last line 51 | 52 | # alternatively: rsstail 53 | # If you use rsstail, uncomment these lines, and comment the above lines withe rssread.py 54 | # $TORIFY rsstail -1 -ldpazPH -u "$1" -n $2 | sed 'H;1h;$!d;x; s/\n\n\n\n\n/\n\n/g' | sed 'H;1h;$!d;x; s/\n\n\n\n/\n\n/g' | \ 55 | # sed 'H;1h;$!d;x; s/\n\n\n/\n\n/g' | sed '/Pub.date: /a \\n\n' # add newlines for separation after last line 56 | } 57 | 58 | if [ "$#" == "0" ]; then 59 | echo -n "Currently supported feeds are: " 60 | echo "all, affaires, andreas, ars1, ars2, ars3, btc, coin, citron, core, futura, hn, jimmy, matrix, noon, pine, qubes, trezor" 61 | echo "Try \"rss pine 2\" for example to get the latest 2 news items from Pine64.org News RSS feed." 62 | exit 0 63 | fi 64 | 65 | arg1=$1 # rss feed, required 66 | arg2=$2 # number of items (optional) or "notorify" 67 | arg3=$3 # "notorify" or empty 68 | 69 | if [ "$arg2" == "" ]; then 70 | # arg2="1" # default, get only last item, if no number specified # rsstail 71 | arg2="" # default, get today's items, if no number specified ## rssread.py 72 | fi 73 | 74 | if [ "$arg2" == "notorify" ] || [ "$arg3" == "notorify" ] || [ "$arg2" == "notor" ] || [ "$arg3" == "notor" ]; then 75 | TORIFY="" 76 | TORPROXY="" 77 | echo "Are you sure you do not want to use TOR?" 78 | if [ "$arg2" == "notorify" ] || [ "$arg2" == "notor" ]; then 79 | # arg2="1" # rsstail 80 | arg2="" # rssread.py, get today's items 81 | fi 82 | fi 83 | 84 | case "$arg2" in 85 | *[!0-9]*) 86 | echo "Second argument is not a number. Skipping. Try \"rss pine 1\"." 87 | exit 0 88 | ;; 89 | *) 90 | # echo "First argument is a number. " 91 | ;; 92 | esac 93 | 94 | function dofeed() { 95 | arg1="$1" 96 | arg2="$2" 97 | case "$arg1" in 98 | all) 99 | for feed in affaires andreas ars1 ars2 ars3 btc citron coin core futura jimmy matrix noon pine qubes trezor; do 100 | dofeed "$feed" "$arg2" 101 | echo -e "\n\n\n" 102 | done 103 | ;; 104 | affaires) 105 | readrss "https://www.lesaffaires.com/rss/techno/internet" "$arg2" 106 | ;; 107 | andreasm) 108 | readrss "https://medium.com/feed/@aantonop" "$arg2" 109 | ;; 110 | andreas) 111 | readrss "https://twitrss.me/twitter_user_to_rss/?user=aantonop" "$arg2" 112 | ;; 113 | ars1) 114 | readrss "http://feeds.arstechnica.com/arstechnica/technology-lab" "$arg2" 115 | ;; 116 | ars2) 117 | readrss "http://feeds.arstechnica.com/arstechnica/features" "$arg2" 118 | ;; 119 | ars3) 120 | readrss "http://feeds.arstechnica.com/arstechnica/gadgets" "$arg2" 121 | ;; 122 | btc) 123 | readrss "https://bitcoin.org/en/rss/blog.xml" "$arg2" 124 | ;; 125 | citron) 126 | TORIFYBEFORE=$TORIFY # temporarily turn TORIFY off because citron does not work under TOR 127 | TORPROXYBEFORE=$TORPROXY # temporarily turn TORPROXY off because citron does not work under TOR 128 | TORIFY="" 129 | TORPROXY="" 130 | readrss "https://www.presse-citron.net/feed/" "$arg2" 131 | TORIFY=$TORIFYBEFORE 132 | TORPROXY=$TORPROXYBEFORE 133 | ;; 134 | coin) 135 | readrss "https://www.coindesk.com/feed?x=1" "$arg2" 136 | ;; 137 | core) 138 | readrss "https://bitcoincore.org/en/rss.xml" "$arg2" 139 | ;; 140 | futura) 141 | # readrss "https://www.futura-sciences.com/rss/actualites.xml" "$arg2" 142 | readrss "https://www.futura-sciences.com/rss/high-tech/actualites.xml" "$arg2" 143 | ;; 144 | hn) 145 | readrss "https://hnrss.org/frontpage" "$arg2" 146 | ;; 147 | jimmy) 148 | readrss "https://medium.com/feed/@jimmysong" "$arg2" 149 | ;; 150 | matrix) 151 | readrss "https://matrix.org/blog/feed/" "$arg2" 152 | ;; 153 | noon) 154 | readrss "https://hackernoon.com/feed" "$arg2" 155 | ;; 156 | nobs) 157 | readrss "http://rss.nobsbtc.com" "$arg2" 158 | ;; 159 | pine) 160 | readrss "https://www.pine64.org/feed/" "$arg2" 161 | ;; 162 | qubes) 163 | readrss "https://www.qubes-os.org/feed.xml" "$arg2" 164 | ;; 165 | trezor) 166 | readrss "https://blog.trezor.io/feed/" "$arg2" 167 | ;; 168 | *) 169 | echo "This feed is not configured on server." 170 | ;; 171 | esac 172 | } 173 | 174 | dofeed "$arg1" "$arg2" 175 | 176 | exit 0 177 | 178 | # EOF 179 | -------------------------------------------------------------------------------- /eno/scripts/rssread.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | """Read and parse RSS feeds.""" 3 | 4 | # Don't change tabbing, spacing, formating as file is automatically linted. 5 | # isort rssread.py 6 | # flake8 rssread.py 7 | # python3 -m black rssread.py 8 | 9 | # forked from source: https://github.com/sathwikv143/rssNews 10 | 11 | # command line options: see: rssread.py --help 12 | # get the post from today 13 | # example: rssread.py --feed "https://hnrss.org/frontpage" --today 14 | # get the post from yesterday 15 | # example: rssread.py --feed "https://hnrss.org/frontpage" --yesterday 16 | # get last 3 posts 17 | # example: rssread.py -feed "https://hnrss.org/frontpage" --number 3 18 | # example: rssread.py -feed https://feed1.io https://feed2.com --today -n 10 19 | # example: rssread.py -f http://feed.com -b 2021-02-20 -e 2021-02-20 -n 1000 20 | 21 | # install dependencies: e.g. similar to: 22 | # pip3 install --user --upgrade feedparser 23 | # pip3 install --user --upgrade fuzzywuzzy 24 | # pip3 install --user --upgrade python-Levenshtein 25 | # pip3 install --user --upgrade python-dateutil 26 | 27 | import argparse 28 | import logging 29 | import os 30 | import re 31 | import sys 32 | import textwrap 33 | import traceback 34 | from datetime import date, timedelta 35 | 36 | import feedparser 37 | import requests 38 | from dateutil import parser 39 | from fuzzywuzzy import fuzz 40 | 41 | DEFAULT_SEPARATOR = "" 42 | DEFAULT_NUMBER = 3 43 | 44 | 45 | def display_news(title, summary, summary_detail, content, link, pubDate): # noqa 46 | """Display the Parsed News.""" 47 | # print(79*"=") 48 | if not args.no_title: 49 | print("Title: " + title) 50 | if not args.no_summary: 51 | for line in textwrap.wrap(summary, width=79): 52 | print(line) 53 | # if summary != "": 54 | # print("") 55 | if not args.no_summary_detail: 56 | for line in textwrap.wrap(summary_detail, width=79): 57 | print(line) 58 | # if summary_detail != "": 59 | # print("") 60 | if not args.no_content: 61 | for line in textwrap.wrap(content, width=79): 62 | print(line) 63 | # if content != "": 64 | # print("") 65 | if not args.no_link: 66 | print("Link: " + link) 67 | if not args.no_date: 68 | print("Pub.date: " + pubDate) 69 | if args.separator != "": 70 | print(args.separator.replace("\\n", "\n"), end="") 71 | 72 | 73 | def get_date(entries): 74 | """Get the date published of an Entry.""" 75 | dop = entries["published"] 76 | dop_to_date = parser.parse(dop, ignoretz=True) 77 | dop_date = dop_to_date.date() 78 | logger.debug(f"published: {dop} and {dop_to_date}") 79 | return dop_date 80 | 81 | 82 | def get_news_entry(entry, proxyindicator): # noqa 83 | """Get the title, link and summary of one news item.""" 84 | try: 85 | title = entry["title"] 86 | except Exception: 87 | title = "Unknown" 88 | pass 89 | try: 90 | link = entry["link"] 91 | except Exception: 92 | link = "" 93 | pass 94 | try: 95 | summary_raw = re.sub("<[^<]+?>", " ", str(entry["summary"]).replace("\n", " "),) 96 | summary = "Summary: " + summary_raw 97 | summary = " ".join(summary.split()) # collapse multiple spaces 98 | except Exception: 99 | summary_raw = "" 100 | summary = "" 101 | pass 102 | try: 103 | summary_detail_raw = "Summary Detail: " + re.sub( 104 | "<[^<]+?>", " ", str(entry["summary_detail"]).replace("\n", " "), 105 | ) 106 | summary_detail = "Summary Detail: " + summary_detail_raw 107 | # collapse multiple spaces into a single space 108 | summary_detail = " ".join(summary_detail.split()) 109 | except Exception: 110 | summary_detail_raw = "" 111 | summary_detail = "" 112 | pass 113 | try: 114 | content = "Content: " + re.sub("<[^<]+?>", " ", str(entry["content"])) 115 | # collapse multiple spaces into a single space 116 | content = " ".join(content.split()) 117 | except Exception: 118 | content = "" 119 | if "DEBUG" in os.environ: 120 | pass # print stacktrace 121 | try: 122 | pubDate = entry["published"] 123 | except Exception: 124 | pubDate = "" 125 | pass 126 | if content.find(summary_raw) == -1: 127 | if "DEBUG" in os.environ: 128 | print("Summary_detail and content are different!") 129 | if fuzz.partial_ratio(summary_raw, content) > 90: 130 | if "DEBUG" in os.environ: 131 | print("Summary_detail and content are very similar!") 132 | content = "" # content is more or less a copy of summary 133 | else: 134 | content = "" # content is just a copy of summary 135 | if len(summary_raw) > 10000: 136 | # if the summary is so big (10K+) I don't care about the 137 | # details anymore 138 | summary_detail = "" 139 | elif summary_detail_raw.find(summary_raw) == -1: 140 | if "DEBUG" in os.environ: 141 | print("Summary_detail and summary are different!") 142 | print(f"Sizes are {len(summary_detail_raw)} " f"and {len(summary_raw)}.") 143 | if len(summary_detail_raw) > 10000 and len(summary_raw) > 10000: 144 | if ( 145 | abs(len(summary_detail_raw) - len(summary_raw)) 146 | / max(len(summary_detail_raw), len(summary_raw)) 147 | < 0.15 148 | ): 149 | # summary_detail is more or less a copy of summary 150 | summary_detail = "" 151 | else: 152 | if ( 153 | fuzz.partial_ratio(summary_detail_raw, summary_raw) > 90 154 | ): # this blows up for large text (20K+) 155 | if "DEBUG" in os.environ: 156 | print("Summary_detail and summary are very similar!") 157 | # summary_detail is more or less a copy of summary 158 | summary_detail = "" 159 | else: 160 | summary_detail = "" # summary_detail is just a copy of summary 161 | display_news( 162 | title + proxyindicator, summary, summary_detail, content, link, pubDate 163 | ) 164 | 165 | 166 | def get_news(entries, noe, fromday, uptoday, parsed_url, proxyindicator): 167 | """Get the title, link and summary of the news.""" 168 | for i in range(0, noe): 169 | logger.debug(f"Entry:: {entries[i]}") 170 | dop_date = get_date(entries[i]) 171 | if dop_date >= fromday and dop_date <= uptoday: 172 | get_news_entry(entries[i], proxyindicator) 173 | 174 | 175 | def parse_url(urls, noe, fromday, uptoday): 176 | """Parse the URLs with feedparser.""" 177 | if args.tor: 178 | if os.name == "nt": 179 | TOR_PORT = 9150 # Windows 180 | else: 181 | TOR_PORT = 9050 # LINUX 182 | proxies = { 183 | "http": f"socks5://127.0.0.1:{TOR_PORT}", 184 | "https": f"socks5://127.0.0.1:{TOR_PORT}", 185 | } 186 | proxyindicator = " [via Tor]" 187 | else: 188 | proxies = {} 189 | proxyindicator = "" 190 | 191 | logger.debug(f"Proxy is: {proxies}{proxyindicator}") 192 | 193 | for url in urls: 194 | # feedparser does NOT support PROXY or Tor 195 | # but it does support files or strings, so we 196 | # load the URL into a string 197 | logger.debug(f"URL is: {url}") 198 | cont = requests.get(url, proxies=proxies) 199 | logger.debug(f"cont is: {cont}") 200 | if args.verbose: 201 | logger.debug(f"cont is: {cont.content}") 202 | parsed_url = feedparser.parse(cont.content) 203 | entries = parsed_url.entries 204 | max = len(entries) 205 | noe = min(noe, max) 206 | get_news(entries, noe, fromday, uptoday, parsed_url, proxyindicator) 207 | 208 | 209 | # main 210 | if __name__ == "__main__": # noqa 211 | logging.basicConfig() # initialize root logger, a must 212 | if "DEBUG" in os.environ: 213 | logging.getLogger().setLevel(logging.DEBUG) # set root logger log level 214 | else: 215 | logging.getLogger().setLevel(logging.INFO) # set root logger log level 216 | 217 | # Construct the argument parser 218 | ap = argparse.ArgumentParser(description="This program reads news from RSS feeds.") 219 | # Add the arguments to the parser 220 | ap.add_argument( 221 | "-d", 222 | "--debug", 223 | required=False, 224 | action="store_true", 225 | help="Print debug information.", 226 | ) 227 | ap.add_argument( 228 | "-v", 229 | "--verbose", 230 | required=False, 231 | action="store_true", 232 | help="Print verbose output.", 233 | ) 234 | ap.add_argument( 235 | "-f", # feed 236 | "--feed", 237 | required=True, 238 | type=str, 239 | nargs="+", 240 | help="Specify RSS feed URL. E.g. --feed https://hnrss.org/frontpage.", 241 | ) 242 | ap.add_argument( 243 | "-o", # onion 244 | "--tor", 245 | required=False, 246 | action="store_true", 247 | help="Use Tor, go through Tor Socks5 proxy.", 248 | ) 249 | ap.add_argument( 250 | "-t", # today 251 | "--today", 252 | required=False, 253 | action="store_true", 254 | help="Get today's entries from RSS feed.", 255 | ) 256 | ap.add_argument( 257 | "-y", # yesterday 258 | "--yesterday", 259 | required=False, 260 | action="store_true", 261 | help="Get yesterday's entries from RSS feed", 262 | ) 263 | ap.add_argument( 264 | "-n", # number 265 | "--number", 266 | required=False, 267 | type=int, 268 | default=DEFAULT_NUMBER, 269 | help=( 270 | "Number of last entries to get from from RSS feed. " 271 | f'Default is "{DEFAULT_NUMBER}".' 272 | ), 273 | ) 274 | ap.add_argument( 275 | "-b", # beginning 276 | "--from-day", 277 | required=False, 278 | type=str, 279 | help=( 280 | "Specify a 'from' date, i.e. an earliest day allowed. " 281 | "Specify in format YYYY-MM-DD such as 2021-02-25." 282 | ), 283 | ) 284 | ap.add_argument( 285 | "-e", # end 286 | "--to-day", 287 | required=False, 288 | type=str, 289 | help=( 290 | "Specify a 'to' date, i.e. a latest day allowed. " 291 | "Specify in format YYYY-MM-DD such as 2021-02-26." 292 | ), 293 | ) 294 | ap.add_argument( 295 | "-s", # separator 296 | "--separator", 297 | required=False, 298 | type=str, 299 | default=DEFAULT_SEPARATOR, 300 | help=( 301 | "Specify a separator to be printed after each RSS entry. " 302 | f'Default is "{DEFAULT_SEPARATOR}". ' 303 | 'E.g. use "\\n" to print an empty line. ' 304 | 'Or use "==================\\n" to print a dashed line. ' 305 | 'Note that letters like "-" or "newline" might need to be ' 306 | 'escaped like "\\-" or "\\n".' 307 | ), 308 | ) 309 | ap.add_argument( 310 | "--no-title", 311 | required=False, 312 | action="store_true", 313 | help=( 314 | 'Don\'t print the "Title" record of the RSS entry. This is ' 315 | "useful for example for feeds where the title is just a " 316 | "repetition of the first words of the summary. " 317 | ), 318 | ) 319 | ap.add_argument( 320 | "--no-summary", 321 | required=False, 322 | action="store_true", 323 | help='Don\'t print the "Summary" record of the RSS entry.', 324 | ) 325 | ap.add_argument( 326 | "--no-summary-detail", 327 | required=False, 328 | action="store_true", 329 | help='Don\'t print the "Summary Detail" record of the RSS entry.', 330 | ) 331 | ap.add_argument( 332 | "--no-content", 333 | required=False, 334 | action="store_true", 335 | help='Don\'t print the "Content" record of the RSS entry.', 336 | ) 337 | ap.add_argument( 338 | "--no-link", 339 | required=False, 340 | action="store_true", 341 | help='Don\'t print the "Link" record of the RSS entry.', 342 | ) 343 | ap.add_argument( 344 | "--no-date", 345 | required=False, 346 | action="store_true", 347 | help='Don\'t print the "Publish Date" record of the RSS entry.', 348 | ) 349 | args = ap.parse_args() 350 | if args.debug: 351 | logging.getLogger().setLevel(logging.DEBUG) # set root logger log level 352 | logging.getLogger().info("Debugging is turned on.") 353 | logger = logging.getLogger("readrss") 354 | # logging.getLogger().info("Debug is turned on.") 355 | 356 | # Get Dates of Present and Previous Day's 357 | today = date.today() 358 | yesterday = today - timedelta(1) 359 | ayearago = today - timedelta(365) 360 | 361 | noe = args.number # initialize 362 | fromday = ayearago 363 | uptoday = today 364 | if args.today: 365 | fromday = today # all entries of today 366 | if args.yesterday: 367 | fromday = yesterday # all entries of yesterday 368 | uptoday = yesterday 369 | 370 | if args.from_day: 371 | fromday = date.fromisoformat(args.from_day) 372 | if args.to_day: 373 | uptoday = date.fromisoformat(args.to_day) 374 | 375 | logger.debug(f"feed(s): {args.feed}") 376 | logger.debug(f"number: {noe}") 377 | logger.debug(f"from day: {fromday}") 378 | logger.debug(f"up to day: {uptoday}") 379 | 380 | try: 381 | parse_url(args.feed, noe, fromday, uptoday) 382 | except requests.exceptions.ConnectionError as e: 383 | if args.tor: 384 | print( 385 | f"ConnectionError: Maybe Tor is not running. ({e})", file=sys.stderr, 386 | ) 387 | else: 388 | print( 389 | "ConnectionError: " f"Maybe network connection is not down. ({e})", 390 | file=sys.stderr, 391 | ) 392 | sys.exit(1) 393 | except Exception: 394 | traceback.print_exc(file=sys.stdout) 395 | sys.exit(1) 396 | except KeyboardInterrupt: 397 | sys.exit(1) 398 | 399 | # EOF 400 | -------------------------------------------------------------------------------- /eno/scripts/s2f.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=" --tor " 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 | arg1=$1 16 | arg2=$2 17 | 18 | if [[ "$arg1" == "+" ]] || [[ "$arg1" == "more" ]] || [[ "$arg1" == "plus" ]] || [[ "$arg1" == "v" ]] || [[ "$arg2" == "+" ]] || [[ "$arg2" == "more" ]] || [[ "$arg2" == "plus" ]] || [[ "$arg2" == "v" ]]; then 19 | FORMAT="" 20 | else 21 | FORMAT="--terse" 22 | fi 23 | 24 | if [[ "${arg1,,}" == "notorify" ]] || [[ "${arg1,,}" == "notor" ]] || [[ "${arg2,,}" == "notorify" ]] || [[ "${arg2,,}" == "notor" ]]; then 25 | # echo "Turning Tor use off." 26 | TORIFY="" 27 | TORPROXY="" 28 | fi 29 | 30 | # s2f must be installed 31 | type s2f.py >/dev/null 2>&1 || { 32 | # it was not found in normal path, lets see if we can amplify the PATH by sourcing profile files 33 | . $HOME/.bash_profile 2>/dev/null 34 | . $HOME/.profile 2>/dev/null 35 | type s2f.py >/dev/null 2>&1 || { 36 | echo "For s2f to work you must first install the file \"s2f.py\" on the server." 37 | echo "Download from https://github.com/8go/bitcoin-stock-to-flow" 38 | echo "Set your PATH if you have installed it but it cannot be found." 39 | exit 0 40 | } 41 | } 42 | 43 | $TORIFY s2f.py $TORPROXY $FORMAT | grep -v "Calculated" | grep -v "Data sources" # this displays nicer with format "code" 44 | 45 | # EOF 46 | -------------------------------------------------------------------------------- /eno/scripts/tides.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 | # xmllint must be installed 12 | type xmllint >/dev/null 2>&1 || { 13 | # see: https://github.com/fcambus/ansiweather 14 | echo "This script requires that you install the packge \"xmllint\" (libxml2-utils) on the server." 15 | exit 0 16 | } 17 | 18 | if [ -f "$(dirname "$0")/config.rc" ]; then 19 | [ "$DEBUG" == "1" ] && echo "Sourcing $(dirname "$0")/config.rc" 20 | # shellcheck disable=SC1090 21 | source "$(dirname "$0")/config.rc" # if it exists, optional, not needed, allows to set env variables 22 | fi 23 | 24 | # $1: location, city 25 | # $2: optional, "full" for more details 26 | function gettide() { 27 | if [[ "$1" == "" ]]; then 28 | echo "No tides report for empty location. Set location." 29 | return 30 | fi 31 | if [[ "$2" == "full" ]] || [[ "$2" == "f" ]] || [[ "$2" == "more" ]] || [[ "$2" == "+" ]]; then 32 | # give a full, long listing of forecast 33 | echo "To be implemented" # to be implemented 34 | elif [[ "$2" == "short" ]] || [[ "$2" == "less" ]] || [[ "$2" == "s" ]] || [[ "$2" == "l" ]] || [[ "$2" == "-" ]]; then 35 | # give a short, terse listing of forecast 36 | echo "To be implemented" # to be implemented 37 | else 38 | # give a mediaum, default listing of tides, just today 39 | echo "${1^}: " 40 | #old code: they changed web layout, so it broke 41 | #xmllint --html --xpath '//table[@class = "tide-times__table--table"]/tbody/tr/td' - 2>/dev/null | \ 42 | #sed "s|<td><b>| |g" | sed "s|</b></td>| |g" | sed "s|<span class=\"today__tide-times--nextrow\">| |g" | \ 43 | #sed "s|</span>| |g" | sed 's|<td class="js-two-units-length-value" data-units="Imperial"><b class="js-two-units-length-value__primary">| |g' | \ 44 | #sed 's|</b><span class="today__tide-times--nextrow js-two-units-length-value__secondary">| |g' | \ 45 | #sed 's|<td class="js-two-units-length-value" data-units="Metric">| |g' | \ 46 | #sed 's|<b class="js-two-units-length-value__primary">| |g' | \ 47 | #sed "s|</td>||g" | tr -d '\n' | sed "s|Low Tide|\nLow Tide|g" | sed "s|High Tide|\nHigh Tide|g" | \ 48 | #sed 's|([^)]*)||g' | sed 's| | |g' | sed 's| Tide | |g' | sed 's|m |m|g' | sed 's| m|m|g' 49 | 50 | x=1 51 | while [ $x -le 10 ]; do 52 | # shellcheck disable=SC2086 53 | res=$($TORIFY wget -q -O - https://www.tide-forecast.com/locations/${1}/tides/latest | 54 | xmllint --html --xpath '//table[@class = "tide-day-tides"]/tr/td' - 2>/dev/null | head -n 12 | sed 'N;N;s/\n/ /g' | sed -e 's/<[^<>]*>//g' | 55 | sed -e 's/Low Tide/Low Tide /g' | sed -e 's/([^()]*ft)//g' | sed -e 's/ m $/m/g' | sed -e 's/(/ (/g') 56 | if [ "$res" != "" ]; then 57 | echo "$res" 58 | break 59 | else 60 | echo "retrying ..." 61 | fi 62 | # get 4 values (each value has 3 lines), so get 12 lines, any large number could be got, even 30, maybe even 60 63 | # sed 'N;N;s/\n/ /g' ... combine lines 1, 2 and 3. 4, 5 and 6. etc 64 | # sed -e 's/<[^<>]*>//g' ... remove everything that is between <...>, i.e. remove the HTML tags 65 | # sed -e 's/([^()]*ft)//g' ... remove the '(3.45 ft)' data 66 | x=$(($x + 1)) 67 | done 68 | echo "" # add newline 69 | fi 70 | } 71 | 72 | if [ "$#" == "0" ]; then 73 | echo "Example tide locations are: hamburg san-diego new-york san-fran" 74 | echo "Try \"tide hamburg\" for example to get the Hamburg tide forecast." 75 | exit 0 76 | fi 77 | 78 | arg1=$1 # tide-location, city, required 79 | arg2=$2 # "full" (optional) or "short" (optional) or "notorify" (optional) or empty 80 | arg3=$3 # "notorify" or empty 81 | 82 | #if [ "$arg2" == "" ]; then 83 | # arg2="1" # default, get only last item, if no number specified 84 | #fi 85 | 86 | if [ "$arg2" == "notorify" ] || [ "$arg3" == "notorify" ]; then 87 | TORIFY="" 88 | echo "Are you sure you do not want to use TOR?" 89 | if [ "$arg2" == "notorify" ]; then 90 | arg2="$arg3" 91 | fi 92 | fi 93 | 94 | function dotide() { 95 | arg1="${1,,}" 96 | arg2="${2,,}" 97 | case "$arg1" in 98 | all) 99 | for city in san-francisco san-diego lima; do 100 | dotide "$city" "$arg2" 101 | echo -e "\n\n\n" 102 | done 103 | ;; 104 | "${TIDES_DEFAULT_CITY1,,}" | "${TIDES_DEFAULT_CITY2,,}" | "${TIDES_DEFAULT_CITY3,,}") 105 | gettide "${TIDES_DEFAULT_CITY1}" "$arg2" 106 | ;; 107 | h | hamburg) 108 | gettide "Hamburg-Germany" "$arg2" 109 | ;; 110 | l | london) 111 | gettide "London-Bridge-England" "$arg2" 112 | ;; 113 | m | melbourne) 114 | gettide "Melbourne-Australia" "$arg2" 115 | ;; 116 | ny | nyc | new-york) 117 | gettide "New-York-New-York" "$arg2" 118 | ;; 119 | p | pornic) 120 | gettide "Pornic" "$arg2" # France 121 | ;; 122 | sd | san-diego) 123 | gettide "San-Diego-California" "$arg2" 124 | ;; 125 | sf | san-fran | san-francisco) 126 | gettide "San-Francisco-California" "$arg2" 127 | ;; 128 | u | urangan | hervey | hb | fraser | "75" | "fi") 129 | gettide "Urangan-Australia" "$arg2" 130 | ;; 131 | *) 132 | gettide "$arg1" "$arg2" 133 | ;; 134 | esac 135 | } 136 | 137 | dotide "$arg1" "$arg2" 138 | 139 | exit 0 140 | 141 | # EOF 142 | -------------------------------------------------------------------------------- /eno/scripts/top.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Top 5 CPU consumers:" 4 | #ps -eo %cpu,pid,ppid,cmd --sort=-%cpu | head 5 | ps -eo %cpu,cmd:60 --sort=-%cpu --cols 80 | head -n 5 6 | echo "" 7 | echo "Top 5 RAM consumers:" 8 | #ps -eo %mem,pid,ppid,cmd --sort=-%mem | head 9 | ps -eo %mem,cmd:60 --sort=-%mem --cols 80 | head -n 5 10 | echo 11 | echo "Details of CPU and Memory usage:" 12 | echo 13 | 14 | declare -A count_array 15 | 16 | count_array["%CPU"]=3 17 | count_array["%MEM"]=3 18 | count_array["TIME+"]=1 19 | # count_array+=(["%MEM"]=3 ["TIME+"]=1) # alternative way of setting array 20 | 21 | # The arr array now contains the three key value pairs. 22 | if [ "$DEBUG" == "true" ]; then 23 | for key in "${!count_array[@]}"; do 24 | echo ${key} ${count_array[${key}]} 25 | done 26 | fi 27 | 28 | top -bn 1 | head -n 5 29 | for key in "${!count_array[@]}"; do 30 | for ii in $(seq ${count_array[${key}]}); do 31 | echo "===sorted by $key ($ii)===" 32 | # -o sort by %CPU %MEM TIME+, -c ... full commnd, -w ... width, -n ... number of times, -b ... batch 33 | # tr -s ... squeeze spaces 34 | # sed ... remove leading whitespaces 35 | # cut 36 | # 3 times: replace first space with | 37 | # column: now column 12 "cmd arg1 arg2 file1 file2" is seen as 1 column even though it has spaces 38 | top -bn 1 -o "$key" -c -w 180 | head -n10 | tail -n4 | tr -s " " | sed -e 's/^[ \t]*//' | cut -d " " -f 9,10,11,12- | sed 's/ /|/1' | sed 's/ /|/1' | sed 's/ /|/1' | column -t -s'|' 39 | done 40 | done 41 | 42 | echo 43 | echo "CPU time used divided by the time the process has been running (cputime/realtime ratio)" 44 | for ss in "%cpu" "%mem" "time"; do 45 | echo "===sorted by $ss===" 46 | ps -eo %cpu,%mem,bsdtime,cmd:60,pid --sort=-$ss | head 47 | done # sort by %mem %cpu time, -o ... format/fields, : ... length, -e ... all processes (-A) 48 | 49 | # EOF 50 | -------------------------------------------------------------------------------- /eno/scripts/totp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Example URI: 4 | # otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example 5 | 6 | # Example TOTP secret key: 7 | # JBSWY3DPEHPK3PXP 8 | 9 | # TOTP secret can be password protected thru a 2nd argument 10 | 11 | # Do this to encypt a TOTP secret key: 12 | # echo "JBSWY3DPEHPK3PXP" | openssl enc -aes-256-cbc -salt -a -pbkdf2 # returns encrypted TOTP key 13 | # "U2FsdGVkX1+etVuH68uNDv1v5J+XfYXRSuuEyypLJrEGCfo4V91eICW1085lQa68" # TOTP secret key "JBSWY3DPEHPK3PXP" encrypted with passphrase "test" 14 | 15 | # the reverse is: decrypted the cipher text to get original TOTP secret key, in the example we use passphrase "test" 16 | # echo "U2FsdGVkX1+etVuH68uNDv1v5J+XfYXRSuuEyypLJrEGCfo4V91eICW1085lQa68" | openssl enc -aes-256-cbc -d -salt -a -pbkdf2 -k test # returns "JBSWY3DPEHPK3PXP" 17 | # JBSWY3DPEHPK3PXP 18 | 19 | # oathtool must be installed 20 | type oathtool >/dev/null 2>&1 || { 21 | echo "This script requires that you install the packge \"oathtool\" on the server." 22 | exit 0 23 | } 24 | 25 | function totp() { 26 | D="$(date +%S)" 27 | # shellcheck disable=SC2001 28 | DNOLEADINGZERO=$(echo "$D" | sed 's/^0//') 29 | # shellcheck disable=SC2004 30 | SECONDSREMAINING=$((30 - $DNOLEADINGZERO % 30)) 31 | # [ "$DEBUG" != "" ] && echo "DEBUG: TOKEN=$1" # NEVER log this! 32 | X=$(oathtool --totp -b "$1") 33 | echo "$SECONDSREMAINING seconds remaining : $X" 34 | } 35 | 36 | if [ "$#" == "0" ]; then 37 | echo "No TOTP nick-name given. Try \"totp example-plaintext\" or \"totp example-encrypted\" next time." 38 | echo "This script has to be set up on the server to be meaningful." 39 | echo "As shipped it only has an example of a TOTP service." 40 | exit 0 41 | fi 42 | 43 | arg1=$1 44 | arg2=$2 45 | 46 | case "$arg1" in 47 | example-plaintext) 48 | # echo "Calculating TOTP PIN for you" 49 | PLAINTEXTTOTPKEY="JBSWY3DPEHPK3PXP" 50 | totp "$PLAINTEXTTOTPKEY" 51 | exit 0 52 | ;; 53 | example-encrypted) 54 | # echo "Calculating TOTP PIN for you" 55 | if [ "$arg2" == "" ]; then 56 | echo "A password is required for this TOTP nick name." 57 | exit 0 58 | fi 59 | CIPHERTOTPKEY="U2FsdGVkX1+etVuH68uNDv1v5J+XfYXRSuuEyypLJrEGCfo4V91eICW1085lQa68" 60 | PLAINTEXTTOTPKEY=$(echo "$CIPHERTOTPKEY" | openssl enc -aes-256-cbc -d -salt -a -pbkdf2 -k "$arg2") 61 | totp "$PLAINTEXTTOTPKEY" 62 | # echo "Using \"$arg2\" as password. Totp secret is \"$TOTPSECRET\"." 63 | exit 0 64 | ;; 65 | *) 66 | echo "Unknown TOTP nick name \"$arg1\". Not configured on server." 67 | ;; 68 | esac 69 | 70 | # EOF 71 | -------------------------------------------------------------------------------- /eno/scripts/twitter.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 | # rsstail must be installed 12 | type rsstail >/dev/null 2>&1 || { 13 | echo "This script requires that you install the packge \"rsstail\" on the server." 14 | exit 0 15 | } 16 | 17 | function readrss() { 18 | feeddata=$($TORIFY rsstail -1 -ldpazPH -u "$1" -n "$2" 2>&1) 19 | if [ "$(echo "$feeddata" | xargs)" == "" ] || 20 | [ "$feeddata" == "Error reading RSS feed: Parser error" ]; then 21 | echo "Can't screenscrape Twitter right now. Rate limits are in place. Come back later. ($1)" 22 | return 1 23 | fi 24 | echo "Fetching latest $2 items from feed \"$1\"..." 25 | # If there are 3 newlines, it will generate separate posts, 26 | # but it is nicer and more compact if everything is nicely bundled into 1 post. 27 | # So, first we use sed to remove all occurances of 5, 4, and 3 newlines. 28 | # Then we insert 2 newlines after the last newline to create 3 newlines, 29 | # so that at the end of the feed item the Matrix message is split. 30 | # This way N feed posts always create exactly N Matrix messages. 31 | # Inserting newlines with sed: 32 | # https://unix.stackexchange.com/questions/429139/replace-newlines-with-sed-a-la-tr 33 | echo "$feeddata" | sed 'H;1h;$!d;x; s/\n\n\n\n\n/\n\n/g' | 34 | sed 'H;1h;$!d;x; s/\n\n\n\n/\n\n/g' | sed 'H;1h;$!d;x; s/\n\n\n/\n\n/g' | 35 | sed '/Pub.date: /a \\n\n' # add newlines for separation after last feed item line 36 | } 37 | 38 | if [ "$#" == "0" ]; then 39 | echo "Example Twitter user names are: aantonom adam3us balajis elonmusk naval NickZsabo4" 40 | echo "Try \"tweet elonmusk 2\" for example to get the latest 2 tweets from Elon." 41 | exit 0 42 | fi 43 | 44 | arg1=$1 # twitter user name, required 45 | arg2=$2 # number of items (optional) or "notorify" 46 | arg3=$3 # "notorify" or empty 47 | 48 | if [ "$arg2" == "" ]; then 49 | arg2="1" # default, get only last item, if no number specified 50 | fi 51 | 52 | if [ "$arg2" == "notorify" ] || [ "$arg3" == "notorify" ]; then 53 | TORIFY="" 54 | echo "Are you sure you do not want to use TOR?" 55 | if [ "$arg2" == "notorify" ]; then 56 | arg2="1" 57 | fi 58 | fi 59 | 60 | case "$arg2" in 61 | '' | *[!0-9]*) 62 | echo "Second argument is not a number. Skipping. Try \"tweet elonmusk 1\"." 63 | exit 0 64 | ;; 65 | *) 66 | # echo "First argument is a number. " 67 | ;; 68 | esac 69 | 70 | function dofeed() { 71 | arg1="$1" 72 | arg2="$2" 73 | case "$arg1" in 74 | all) 75 | for feed in aantonop adam3us balajis Blockstream elonmusk naval jimmysong NickZsabo4; do 76 | dofeed "$feed" "$arg2" 77 | if [ "$?" == "1" ]; then 78 | echo "Giving up after first error. Sorry." 79 | return 1 80 | fi 81 | echo -e "\n\n\n" 82 | done 83 | ;; 84 | *) 85 | readrss "https://twitrss.me/twitter_user_to_rss/?user=${arg1}" "$arg2" 86 | ;; 87 | esac 88 | } 89 | 90 | dofeed "$arg1" "$arg2" 91 | 92 | exit 0 93 | 94 | # EOF 95 | -------------------------------------------------------------------------------- /eno/scripts/update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$#" == "0" ]; then 4 | echo "You must be updating something. Try \"update os\"." 5 | echo "\"bot\", \"matrix\", \"os\", and \"world\" are configured on server." 6 | exit 0 7 | fi 8 | arg1=$1 9 | case "${arg1,,}" in 10 | "bot" | "eno" | "mybot") 11 | echo "Sorry. The bot update is not implemented. If you really want to update your bot, add your update code here." 12 | ;; 13 | "matrix") 14 | echo "The bot will update the matrix software." 15 | # the name of the service might vary based on installation from : synapse-matrix, matrix, etc. 16 | # there are many different ways to install matrix and to update matrix. 17 | echo "Sorry. The matrix update is not implemented. Add it here if desired." 18 | ;; 19 | "os") 20 | echo "The bot will update the operating system" 21 | 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 22 | # dnf OR apt exists 23 | type dnf >/dev/null 2>&1 || { 24 | sudo apt-get update || 25 | { 26 | echo "Error while using apt. Maybe due to missing permissions?" 27 | return 0 28 | } 29 | sudo apt-get --yes --with-new-pkgs upgrade || 30 | { 31 | echo "Error while using apt. Maybe due to missing permissions?" 32 | return 0 33 | } 34 | sudo apt-get --yes autoremove || 35 | { 36 | echo "Error while using apt. Maybe due to missing permissions?" 37 | return 0 38 | } 39 | sudo apt-get --yes autoclean || 40 | { 41 | echo "Error while using apt. Maybe due to missing permissions?" 42 | return 0 43 | } 44 | } 45 | type apt >/dev/null 2>&1 || { 46 | sudo dnf -y upgrade || 47 | { 48 | echo "Error while using dnf. Maybe due to missing permissions?" 49 | return 0 50 | } 51 | } 52 | ;; 53 | "world") 54 | echo "Your bot will update world order. World 2.0 ready!" 55 | ;; 56 | *) 57 | echo "This bot does not know how to upgrade ${arg1}." 58 | echo "Only \"bot\", \"matrix\", \"os\", and \"world\" are configured on server." 59 | ;; 60 | esac 61 | 62 | # EOF 63 | -------------------------------------------------------------------------------- /eno/scripts/users.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # PUT YOUR CORRECT ACCESS TOKEN HERE, THIS ACCESS TOKEN MUST HAVE ADMIN RIGHTS 4 | # see documentation how to add admin rights to normal access token 5 | MYACCESSTOKENWITHADMINPERMISSIONS="VERY-LONG-CRYTOGRAHIC-STRING-THAT-IS-YOUR-ACCESS-TOKEN-WITH-ADMIN-PERMISSIONS" 6 | MYHOMESERVER="https://matrix.example.com" 7 | # or put these 2 vars into the ./config.rc config file as 8 | # variables USERS_MYACCESSTOKENWITHADMINPERMISSIONS and USERS_MYHOMESERVER 9 | 10 | if [ -f "$(dirname "$0")/config.rc" ]; then 11 | [ "$DEBUG" == "1" ] && echo "Sourcing $(dirname "$0")/config.rc" 12 | # shellcheck disable=SC1090 13 | source "$(dirname "$0")/config.rc" # if it exists, optional, not needed, allows to set env variables 14 | MYACCESSTOKENWITHADMINPERMISSIONS="${USERS_MYACCESSTOKENWITHADMINPERMISSIONS}" 15 | MYHOMESERVER="${USERS_MYHOMESERVER}" 16 | fi 17 | 18 | if [[ "$MYACCESSTOKENWITHADMINPERMISSIONS" == "" ]] || [[ "$MYHOMESERVER" == "" ]]; then 19 | echo "Either homeserver or access token not provided. Cannot list users without them." 20 | exit 21 | fi 22 | 23 | # myMatrixListUsersSql.sh | cut -d '|' -f 1 | grep -v myMatrixListUsersSql.sh 24 | # If I call sqlite3 here it locks the db and on second+ call it gives error stating that db is locked. 25 | # So, it is better to use the official Matrix Synapse REST API. 26 | 27 | # echo "$MYACCESSTOKENWITHADMINPERMISSIONS" | 28 | # myMatrixListUsersJson.sh | grep -v myMatrixListUsersJson.sh | grep -v "https://" | jq '.users[] | .name' | tr -d '"' 29 | echo "List of at most 100 users:" 30 | curl --silent --header "Authorization: Bearer $MYACCESSTOKENWITHADMINPERMISSIONS" "$MYHOMESERVER/_synapse/admin/v2/users?from=0&limit=100&guests=false" | 31 | jq '.users[] | .name' | tr -d '"' 32 | 33 | # EOF 34 | -------------------------------------------------------------------------------- /eno/scripts/wake.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Put your password hash here 4 | PASSWD_HASH="PutYourPasswordHashHere0123456789abcdef0123456789abcdef012345678" 5 | # Put the MAC address of your PC that you want to wake up here, configure its BIOS for Wake-on-LAN 6 | MAC1="Ma:cO:fY:ou:rP:c0" 7 | # Nickname of PC to wake up 8 | NICK1A="myPc1" 9 | NICK1A="myPcA" 10 | 11 | # or put these 4 vars into the ./config.rc config file as 12 | # variables WAKE_PASSWD_HASH, WAKE_MAC1, WAKE_NICK1A and WAKE_NICK1B 13 | 14 | if [ -f "$(dirname "$0")/config.rc" ]; then 15 | [ "$DEBUG" == "1" ] && echo "Sourcing $(dirname "$0")/config.rc" 16 | # shellcheck disable=SC1090 17 | source "$(dirname "$0")/config.rc" # if it exists, optional, not needed, allows to set env variables 18 | PASSWD_HASH="$WAKE_PASSWD_HASH" 19 | MAC1="$WAKE_MAC1" 20 | NICK1A="$WAKE_NICK1A" 21 | NICK1B="$WAKE_NICK1B" 22 | fi 23 | 24 | if [ "$#" == "0" ]; then 25 | echo "You must specify which PC to wake up. Try \"wake myPc1\" or similar." 26 | exit 0 27 | fi 28 | 29 | if [[ "$MAC1" == "" ]]; then 30 | echo "MAC address not provided. Cannot wake PC without it." 31 | exit 32 | fi 33 | 34 | arg1=$1 35 | arg2=$2 36 | 37 | # $1: which PC to wake up 38 | # $2: possible password 39 | function dowake() { 40 | arg1="$1" 41 | arg2="$2" 42 | case "${arg1,,}" in 43 | "world") 44 | echo "Waking up the world. Good morning Earth!" 45 | ;; 46 | "$NICK1A" | "$NICK1B") 47 | # in order to wake up host, one must provide a password 48 | # we compare the hashes here 49 | if [ "$(echo "$arg2" | sha256sum | cut -d ' ' -f 1)" == "$PASSWD_HASH" ]; then 50 | echo "The bot will wake up host \"$arg1\"." 51 | wakeonlan "$MAC1" 52 | else 53 | echo "Argument missing or argument wrong. Command ignored due to lack of permissions." 54 | fi 55 | ;; 56 | *) 57 | echo "The bot does not know how to wake up host \"${arg1}\"." 58 | echo "Only hosts \"$NICK1A\" and \"$NICK1B\" are configured on server." 59 | ;; 60 | esac 61 | } 62 | 63 | dowake "$arg1" "$arg2" 64 | 65 | exit 0 66 | 67 | # EOF 68 | -------------------------------------------------------------------------------- /eno/scripts/waves.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 | # xmllint must be installed 12 | type xmllint >/dev/null 2>&1 || { 13 | # see: https://github.com/fcambus/ansiweather 14 | echo "This script requires that you install the packge \"xmllint\" (libxml2-utils) on the server." 15 | exit 0 16 | } 17 | 18 | if [ -f "$(dirname "$0")/config.rc" ]; then 19 | [ "$DEBUG" == "1" ] && echo "Sourcing $(dirname "$0")/config.rc" 20 | # shellcheck disable=SC1090 21 | source "$(dirname "$0")/config.rc" # if it exists, optional, not needed, allows to set env variables 22 | fi 23 | 24 | # $1: location, city 25 | # $2: optional, "full" for more details 26 | function getwaves() { 27 | if [[ "$1" == "" ]]; then echo "No waves report for empty location. Set location."; return; fi 28 | if [[ "$2" == "full" ]] || [[ "$2" == "f" ]] || [[ "$2" == "more" ]] || [[ "$2" == "+" ]]; then 29 | # give a full, long listing of forecast 30 | echo "To be implemented" # to be implemented 31 | elif [[ "$2" == "short" ]] || [[ "$2" == "less" ]] || [[ "$2" == "s" ]] || [[ "$2" == "l" ]] || [[ "$2" == "-" ]]; then 32 | # give a short, terse listing of forecast 33 | echo "To be implemented" # to be implemented 34 | else 35 | # give a mediaum, default listing of waves/surf, just today 36 | echo "========== ${1%%/*} ==========" 37 | 38 | x=1 # try 10 times 39 | while [ $x -le 10 ]; do 40 | # shellcheck disable=SC2086 41 | fullpage=$($TORIFY wget -q -O - https://magicseaweed.com/${1} 2>/dev/null) 42 | nowsummaryblock=$(echo $fullpage | xmllint --html --xpath '//div[@class="msw-col-fluid-inner"]/div[@class="row margin-bottom"]/div[@class="col-lg-7 col-md-7 col-sm-12 col-xs-12 msw-fc-current"]/div[@class="row"]/div[@class="col-lg-7 col-md-7 col-sm-7 col-xs-12"]' - 2>/dev/null) 43 | wavesnow=$(echo $fullpage | xmllint --html --xpath '//div[@class="msw-col-fluid-inner"]/div[@class="row margin-bottom"]/div[@class="col-lg-7 col-md-7 col-sm-12 col-xs-12 msw-fc-current"]/div[@class="row"]/div[@class="col-lg-7 col-md-7 col-sm-7 col-xs-12"]/ul[@class="rating rating-large clearfix"]/li[1]' - 2>/dev/null | sed -e 's/<[^>]*>//g' | xargs 2>/dev/null) 44 | weathernow=$(echo $nowsummaryblock | xmllint --html --xpath '//p[1]' - 2>/dev/null | sed -e 's/<[^>]*>//g' | sed -e 's/°c/C/g' | sed -e 's/°f/F/g' | sed -e 's/°c/C/g' | sed -e 's/°f/F/g' | sed -e 's/Â//g' | sed -e '1!b;s/^ /Wind /' | sed -e 's/ /; Weather /g' | sed -e 's/Air/; Air Temp /g' | sed -e 's/Sea/; Sea Temp /g' | tr -s ' ' | sed -e 's/ ;/;/g' | sed -e 's/ $//' | sed -e 's/ C$/C/' | sed -e 's/ F$/F/' | sed -e 's/; Weather//' | sed -e 's/^ //') 45 | # todayblock=$(echo $fullpage | xmllint --html --xpath '//div[@class="scrubber-bars-container"]/div[@class="row margin-bottom"]/div[@class="col-lg-7 col-md-7 col-sm-12 col-xs-12 msw-fc-current"]/div[@class="row"]/div[@class="col-lg-7 col-md-7 col-sm-7 col-xs-12"]' - 2>/dev/null) 46 | # old version: till May 2022: todayconditions=$(echo $fullpage | xmllint --html --xpath '//div[@class="table-responsive-xs"]/table/tbody[1]/tr[@class=" is-first row-hightlight"]' - 2>/dev/null | sed -e 's/°c/C/g' | sed -e 's/°f/F/g' | sed -e 's/°c/C/g' | sed -e 's/°f/F/g' | sed -e 's/<[^>]*>//g' | sed -e 's/%/%\n/g' | column -t -s' ') 47 | # index 4 is 6am, index 5 is 9am, index 6 is noon, index 7 is 3pm, index 8 is 6pm 48 | line=$(echo -e "Time\tSurf\tSwell\tFr\t\tWind\tAir") # Title, Heading of Table 49 | for i in 4 5 6 7 8 ; do 50 | todaytablerow=$(echo $fullpage | xmllint --html --xpath "//table[@class='table table-primary table-forecast allSwellsActive msw-js-table msw-units-large']/tbody[1]/tr[contains(@class,'is-first')][$i]" - 2> /dev/null) 51 | t=$todaytablerow 52 | lineTime=$(echo $t | xmllint --html --xpath '//td[1]/small[1]/text()' - 2>/dev/null) # echo 6am 53 | lineSurf=$(echo $t | xmllint --html --xpath '//td[2]' - 2>/dev/null | sed 's|</b>|-|g' | sed 's|<[^>]*>||g' | xargs | tr -s " ") # echo 0.5-0.8m for surf 54 | lineSwel=$(echo $t | xmllint --html --xpath '//td[4]' - 2>/dev/null | sed 's|</b>|-|g' | sed 's|<[^>]*>||g' | xargs | tr -s " ") # echo 0.5-0.8m for swell 55 | linePeri=$(echo $t | xmllint --html --xpath '//td[5]' - 2>/dev/null | sed 's|</b>|-|g' | sed 's|<[^>]*>||g' | xargs | tr -s " ") # echo 9s as wave period 56 | lineTemp=$(echo $t | xmllint --html --xpath '//td[last()-1]' - 2>/dev/null | sed 's|</b>|-|g' | sed 's|<[^>]*>||g' | xargs | sed -e 's/Â//g' | sed -e 's/°c/C/g' | sed -e 's/°f/F/g' | sed -e 's/°c/C/g' | sed -e 's/°f/F/g' | tr -s " ") # echo 18C as temperature 57 | lineWind=$(echo $t | xmllint --html --xpath '//td[last()-4]' - 2>/dev/null | sed 's|</b>|-|g' | sed 's|<[^>]*>||g' | xargs | tr -s " " | sed -e 's/ /-/' | sed -e 's/ //') # echo 22 34 kph as wind 58 | line=$(echo -e "$line\n$lineTime\t$lineSurf\t$lineSwel\t$linePeri\t$lineWind\t$lineTemp") 59 | done 60 | todayconditions=$line 61 | if [ "$wavesnow" != "" ]; then 62 | echo "$wavesnow" 63 | echo "$weathernow" 64 | echo -e "$todayconditions" | column -t 65 | break 66 | else 67 | echo "retrying ..." 68 | fi 69 | x=$(($x + 1)) 70 | done 71 | # echo "" # add newline 72 | fi 73 | } 74 | 75 | if [ "$#" == "0" ]; then 76 | echo "Example waves or surf locations are: bondi san-diego new-york san-fran" 77 | echo "Try \"waves gunnamatta\" for example to get the Gunnamatta, Melbourne, waves and surf forecast." 78 | exit 0 79 | fi 80 | 81 | arg1=$1 # waves-location, beach/city, required 82 | arg2=$2 # "full" (optional) or "short" (optional) or "notorify" (optional) or empty 83 | arg3=$3 # "notorify" or empty 84 | 85 | #if [ "$arg2" == "" ]; then 86 | # arg2="1" # default, get only last item, if no number specified 87 | #fi 88 | 89 | if [ "$arg2" == "notorify" ] || [ "$arg3" == "notorify" ]; then 90 | TORIFY="" 91 | echo "Are you sure you do not want to use TOR?" 92 | if [ "$arg2" == "notorify" ]; then 93 | arg2="$arg3" 94 | fi 95 | fi 96 | 97 | function dowaves() { 98 | arg1="${1,,}" 99 | arg2="${2,,}" 100 | case "$arg1" in 101 | all) 102 | for city in san-francisco san-diego puerto-rico; do 103 | dowaves "$city" "$arg2" 104 | echo -e "\n\n\n" 105 | done 106 | ;; 107 | "${WAVES_DEFAULT_CITY1,,}" | "${WAVES_DEFAULT_CITY2,,}" | "${WAVES_DEFAULT_CITY3,,}") 108 | getwaves "${WAVES_DEFAULT_CITY1}" "$arg2" 109 | ;; 110 | bondi | sydney) 111 | getwaves "Sydney-Bondi-Surf-Report/996/" "$arg2" 112 | ;; 113 | e | eastbourne | england) 114 | getwaves "Eastbourne-Surf-Report/1325/" "$arg2" 115 | ;; 116 | g | m | gunnamatta | melbourne) 117 | getwaves "Gunnamatta-Surf-Report/535/" "$arg2" 118 | ;; 119 | ny | nyc | new-york | new-jersey) 120 | getwaves "New-Jersey-New-York-Surf-Forecast/22/" "$arg2" 121 | ;; 122 | p | pornic | ermitage | france) 123 | getwaves "LErmitage-Surf-Report/4410/" "$arg2" # France 124 | ;; 125 | pr | puerto-rico | san-juan) 126 | getwaves "Dunes-Puerto-Rico-Surf-Report/452/" "$arg2" 127 | ;; 128 | s | sylt | germany) 129 | getwaves "Sylt-Surf-Report/158/" "$arg2" 130 | ;; 131 | sd | san-diego) 132 | getwaves "Mission-Beach-San-Diego-Surf-Report/297/" "$arg2" 133 | ;; 134 | sf | san-fran | san-francisco) 135 | getwaves "Ocean-Beach-Surf-Report/255/" "$arg2" 136 | ;; 137 | u | urangan | hervey | hb | "75" | fraser | "fi") 138 | getwaves "Fraser-Island-Surf-Report/1002/" "$arg2" 139 | ;; 140 | *) 141 | getwaves "$arg1" "$arg2" 142 | ;; 143 | esac 144 | } 145 | 146 | dowaves "$arg1" "$arg2" 147 | 148 | exit 0 149 | 150 | # EOF 151 | -------------------------------------------------------------------------------- /eno/scripts/weather.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 | # ansiweather must be installed 12 | type ansiweather >/dev/null 2>&1 || { 13 | # see: https://github.com/fcambus/ansiweather 14 | echo "This script requires that you install the packge \"ansiweather\" on the server." 15 | exit 0 16 | } 17 | 18 | if [ -f "$(dirname "$0")/config.rc" ]; then 19 | [ "$DEBUG" == "1" ] && echo "Sourcing $(dirname "$0")/config.rc" 20 | # shellcheck disable=SC1090 21 | source "$(dirname "$0")/config.rc" # if it exists, optional, not needed, allows to set env variables 22 | fi 23 | 24 | # $1: location, city 25 | # $2: optional, "full" for more details 26 | function getweather() { 27 | # if $1 is "" then it will get weather for local location based on IP address 28 | if [[ "$2" == "full" ]] || [[ "$2" == "f" ]] || [[ "$2" == "more" ]] || [[ "$2" == "+" ]]; then 29 | # give a full, long listing of forecast 30 | torify curl "wttr.in/${1%,*}?m" # remove everything to the right of comma 31 | elif [[ "$2" == "short" ]] || [[ "$2" == "less" ]] || [[ "$2" == "s" ]] || [[ "$2" == "l" ]] || [[ "$2" == "-" ]]; then 32 | # give a short, terse listing of forecast 33 | torify curl "wttr.in/${1%,*}?m&format=%l:+%C+%t+(%f)+%o+%p" # remove everything to the right of comma 34 | else 35 | # give a mediaum, default listing of forecast 36 | if [ "${1,,}" == "san+juan,puerto+rico" ]; then set -- "4568138"; fi # bug, reset $1 37 | # see https://openweathermap.org/city/4568138 38 | $TORIFY ansiweather -l "$1" | tr "-" "\n" | tr "=>" "\n" | sed "s/^ //g" | sed '/^[[:space:]]*$/d' 2>&1 39 | fi 40 | } 41 | 42 | if [ "$#" == "0" ]; then 43 | echo "Example weather locations are: paris london san-diego new-york" 44 | echo "Try \"weather melbourne\" for example to get the Melbourne weather forecast." 45 | echo "Try \"weather France\" for example to get the French weather forecast." 46 | echo "Try \"weather New+York,US\" for example to get the NYC weather forecast." 47 | echo "Try \"weather all\" for a selection of weather forecasts." 48 | exit 0 49 | fi 50 | 51 | arg1=$1 # tweather-location, city, required 52 | arg2=$2 # "full" (optional) or "short" (optional) or "notorify" (optional) or empty 53 | arg3=$3 # "notorify" or empty 54 | 55 | #if [ "$arg2" == "" ]; then 56 | # arg2="1" # default, get only last item, if no number specified 57 | #fi 58 | 59 | if [ "$arg2" == "notorify" ] || [ "$arg3" == "notorify" ]; then 60 | TORIFY="" 61 | echo "Are you sure you do not want to use TOR?" 62 | if [ "$arg2" == "notorify" ]; then 63 | arg2="$arg3" 64 | fi 65 | fi 66 | 67 | function doweather() { 68 | arg1="${1,,}" 69 | arg2="${2,,}" 70 | case "$arg1" in 71 | all) 72 | doweather "san-francisco" "$arg2" 73 | for city in san-diego vacaville lima,pe madrid,es San+Juan,Puerto+Rico; do 74 | echo "------------------------------" 75 | doweather "$city" "$arg2" 76 | done 77 | ;; 78 | "${WEATHER_DEFAULT_CITY1,,}" | "${WEATHER_DEFAULT_CITY2,,}" | "${WEATHER_DEFAULT_CITY3,,}") 79 | getweather "${WEATHER_DEFAULT_CITY1}" "$arg2" 80 | ;; 81 | bi | bilbao) 82 | getweather "Bilbao,ES" "$arg2" 83 | ;; 84 | l | lima) 85 | getweather "Lima,PE" "$arg2" 86 | ;; 87 | lo | london) 88 | getweather "London,GB" "$arg2" 89 | ;; 90 | m | melbourne) 91 | getweather "Melbourne,AU" "$arg2" 92 | ;; 93 | ny | nyc | new-york) 94 | getweather "New+York,US" "$arg2" 95 | ;; 96 | p | paris) 97 | getweather "Paris,FR" "$arg2" 98 | ;; 99 | pr | puertorico) 100 | getweather "San+Juan,Puerto+Rico" "$arg2" 101 | ;; 102 | sd | san-diego) 103 | getweather "San+Diego,US" "$arg2" 104 | ;; 105 | sf | san-fran | san-francisco) 106 | getweather "San+Francisco,US" "$arg2" 107 | ;; 108 | sk | st-kilda) 109 | getweather "St+Kilda,AU" "$arg2" 110 | ;; 111 | u | urangan | hervey | hb) # https://openweathermap.org/city/2146219 112 | getweather "Torquay,AU" "$arg2" 113 | ;; 114 | v | vacaville) 115 | getweather "Vacaville,US" "$arg2" 116 | ;; 117 | w | vienna) 118 | getweather "Vienna,AT" "$arg2" 119 | ;; 120 | *) 121 | getweather "$arg1" "$arg2" 122 | ;; 123 | esac 124 | } 125 | 126 | doweather "$arg1" "$arg2" 127 | 128 | exit 0 129 | 130 | # EOF 131 | -------------------------------------------------------------------------------- /eno/scripts/web.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 | # w3m must be installed 12 | type w3m >/dev/null 2>&1 || { 13 | echo "This script requires that you install the packge \"w3m\" on the server." 14 | exit 0 15 | } 16 | 17 | if [ "$#" == "0" ]; then 18 | echo "You must be browsing some URL. Try \"web news.ycombinator.com\"." 19 | # echo "If torify is available all traffic will go through TOR by default." 20 | # echo "If you really must skip TOR, try \"web notorify news.ycombinator.com\"." 21 | exit 0 22 | fi 23 | 24 | if [ "$1" == "notorify" ]; then 25 | TORIFY="" 26 | shift # skip $1 27 | fi 28 | 29 | # if $1 is a number we must skip it, w3m will try to open port 30 | # when calling w3m just give it 1 argument 31 | case $1 in 32 | ''|*[!0-9]*) $TORIFY w3m -dump "$1" ;; 33 | *) echo "Not a valid URL" ;; 34 | esac 35 | 36 | # EOF 37 | -------------------------------------------------------------------------------- /eno/scripts/whoami.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """whoami.""" 4 | 5 | import os 6 | from getpass import getuser 7 | 8 | if __name__ == "__main__": 9 | print(f"- user name: `{getuser()}`\n" 10 | f"- home: `{os.environ['HOME']}`\n" 11 | f"- path: `{os.environ['PATH']}`") 12 | -------------------------------------------------------------------------------- /eno/scripts/xmr.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/monero/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, 32.330680167783 25 | # real_volume_last_24_hours, 1455108.5501404 26 | # percent_change_usd_last_24_hours, 1.152004386319294 27 | XMRBTC=$(echo "$BASELIST" | grep price_btc | cut -d "," -f 2) 28 | 29 | BASELIST2=$($TORIFY curl $TORPROXY --silent --compressed "https://data.messari.io/api/v1/assets/ethereum/metrics" | 30 | jq '.data.market_data | keys_unsorted[] as $k | "\($k), \(.[$k] )"' | 31 | grep -e price_usd -e real_volume_last_24_hours -e percent_change_usd_last_24_hours -e price_btc | tr -d "\"") 32 | # returns something like 33 | # price_usd, 32.330680167783 34 | # real_volume_last_24_hours, 1455108.5501404 35 | # percent_change_usd_last_24_hours, 1.152004386319294 36 | ETHBTC=$(echo "$BASELIST2" | grep price_btc | cut -d "," -f 2) # price of ETH in BTC 37 | 38 | LC_ALL=en_US.UTF-8 printf "Price: %'.0f USD\n" "$(echo "$BASELIST" | grep price_usd | cut -d "," -f 2)" 39 | LC_ALL=en_US.UTF-8 printf "Price: %'.4f BTC\n" "$(echo "$BASELIST" | grep price_btc | cut -d "," -f 2)" 40 | LC_ALL=en_US.UTF-8 printf "Price: %'.4f ETH\n" "$(echo "scale=4 ; $XMRBTC / $ETHBTC" | bc | cut -d "," -f 2)" # price of XMR in ETH 41 | LC_ALL=en_US.UTF-8 printf "Change: %'.1f %%\n" "$(echo "$BASELIST" | grep percent_change_usd_last_24_hours | cut -d "," -f 2)" 42 | LC_ALL=en_US.UTF-8 printf "Volume: %'.0f USD\n" "$(echo "$BASELIST" | grep real_volume_last_24_hours | cut -d "," -f 2)" 43 | 44 | # EOF 45 | -------------------------------------------------------------------------------- /errors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | r"""errors.py. 4 | 5 | 0123456789012345678901234567890123456789012345678901234567890123456789012345678 6 | 0000000000111111111122222222223333333333444444444455555555556666666666777777777 7 | 8 | # errors.py 9 | 10 | Don't change tabbing, spacing, or formating as the 11 | file is automatically linted and beautified. 12 | 13 | """ 14 | 15 | 16 | class ConfigError(RuntimeError): 17 | """Error encountered during reading the config file. 18 | 19 | Arguments: 20 | --------- 21 | msg (str): The message displayed to the user on error 22 | 23 | """ 24 | 25 | def __init__(self, msg): 26 | """Set up.""" 27 | super(ConfigError, self).__init__("%s" % (msg,)) 28 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | r"""main.py. 4 | 5 | 0123456789012345678901234567890123456789012345678901234567890123456789012345678 6 | 0000000000111111111122222222223333333333444444444455555555556666666666777777777 7 | 8 | # main.py 9 | 10 | This file implements the following 11 | - create a Matrix client device if necessary 12 | - logs into Matrix as client 13 | - sets up event managers for messages, invites, emoji verification 14 | - enters the event loop 15 | 16 | Don't change tabbing, spacing, or formating as the 17 | file is automatically linted and beautified. 18 | 19 | """ 20 | 21 | import asyncio 22 | import logging 23 | import sys 24 | import traceback 25 | from time import sleep 26 | from nio import ( 27 | AsyncClient, 28 | AsyncClientConfig, 29 | RoomMessageText, 30 | RoomMessageAudio, 31 | RoomEncryptedAudio, 32 | InviteMemberEvent, 33 | LoginError, 34 | LocalProtocolError, 35 | UpdateDeviceError, 36 | KeyVerificationEvent, 37 | ) 38 | from aiohttp import ( 39 | ServerDisconnectedError, 40 | ClientConnectionError 41 | ) 42 | from callbacks import Callbacks 43 | from config import Config 44 | from storage import Storage 45 | 46 | logger = logging.getLogger(__name__) 47 | 48 | 49 | async def main(): # noqa 50 | """Create bot as Matrix client and enter event loop.""" 51 | # Read config file 52 | # A different config file path can be specified 53 | # as the first command line argument 54 | if len(sys.argv) > 1: 55 | config_filepath = sys.argv[1] 56 | else: 57 | config_filepath = "config.yaml" 58 | config = Config(config_filepath) 59 | 60 | # Configure the database 61 | store = Storage(config.database_filepath) 62 | 63 | # Configuration options for the AsyncClient 64 | client_config = AsyncClientConfig( 65 | max_limit_exceeded=0, 66 | max_timeouts=0, 67 | store_sync_tokens=True, 68 | encryption_enabled=True, 69 | ) 70 | 71 | # Initialize the matrix client 72 | client = AsyncClient( 73 | config.homeserver_url, 74 | config.user_id, 75 | device_id=config.device_id, 76 | store_path=config.store_filepath, 77 | config=client_config, 78 | ) 79 | 80 | # Set up event callbacks 81 | callbacks = Callbacks(client, store, config) 82 | client.add_event_callback(callbacks.message, (RoomMessageText,)) 83 | if config.accept_invitations: 84 | client.add_event_callback(callbacks.invite, (InviteMemberEvent,)) 85 | if config.process_audio: 86 | client.add_event_callback(callbacks.audio, (RoomMessageAudio,RoomEncryptedAudio)) 87 | client.add_to_device_callback( 88 | callbacks.to_device_cb, (KeyVerificationEvent,)) 89 | 90 | # Keep trying to reconnect on failure (with some time in-between) 91 | while True: 92 | try: 93 | # Try to login with the configured username/password 94 | try: 95 | if config.access_token: 96 | logger.debug("Using access token from config file to log " 97 | f"in. access_token={config.access_token}") 98 | 99 | client.restore_login( 100 | user_id=config.user_id, 101 | device_id=config.device_id, 102 | access_token=config.access_token 103 | ) 104 | else: 105 | logger.debug("Using password from config file to log in.") 106 | login_response = await client.login( 107 | password=config.user_password, 108 | device_name=config.device_name, 109 | ) 110 | 111 | # Check if login failed 112 | if type(login_response) == LoginError: 113 | logger.error("Failed to login: " 114 | f"{login_response.message}") 115 | return False 116 | logger.info((f"access_token of device {config.device_name}" 117 | f" is: \"{login_response.access_token}\"")) 118 | except LocalProtocolError as e: 119 | # There's an edge case here where the user hasn't installed 120 | # the correct C dependencies. In that case, a 121 | # LocalProtocolError is raised on login. 122 | logger.fatal( 123 | "Failed to login. " 124 | "Have you installed the correct dependencies? " 125 | "https://github.com/poljar/matrix-nio#installation " 126 | "Error: %s", e 127 | ) 128 | return False 129 | 130 | # Login succeeded! 131 | logger.debug(f"Logged in successfully as user {config.user_id} " 132 | f"with device {config.device_id}.") 133 | # Sync encryption keys with the server 134 | # Required for participating in encrypted rooms 135 | if client.should_upload_keys: 136 | await client.keys_upload() 137 | 138 | if config.change_device_name: 139 | content = {"display_name": config.device_name} 140 | resp = await client.update_device(config.device_id, 141 | content) 142 | if isinstance(resp, UpdateDeviceError): 143 | logger.debug(f"update_device failed with {resp}") 144 | else: 145 | logger.debug(f"update_device successful with {resp}") 146 | 147 | if config.trust_own_devices: 148 | await client.sync(timeout=30000, full_state=True) 149 | # Trust your own devices automatically. 150 | # Log it so it can be manually checked 151 | for device_id, olm_device in client.device_store[ 152 | config.user_id].items(): 153 | logger.debug("My other devices are: " 154 | f"device_id={device_id}, " 155 | f"olm_device={olm_device}.") 156 | logger.info("Setting up trust for my own " 157 | f"device {device_id} and session key " 158 | f"{olm_device.keys['ed25519']}.") 159 | client.verify_device(olm_device) 160 | 161 | await client.sync_forever(timeout=30000, full_state=True) 162 | 163 | except (ClientConnectionError, ServerDisconnectedError): 164 | logger.warning( 165 | "Unable to connect to homeserver, retrying in 15s...") 166 | 167 | # Sleep so we don't bombard the server with login requests 168 | sleep(15) 169 | finally: 170 | # Make sure to close the client connection on disconnect 171 | await client.close() 172 | 173 | try: 174 | asyncio.run(main()) 175 | except Exception: 176 | logger.debug(traceback.format_exc()) 177 | sys.exit(1) 178 | except KeyboardInterrupt: 179 | logger.debug("Received keyboard interrupt.") 180 | sys.exit(1) 181 | -------------------------------------------------------------------------------- /message_responses.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | r"""message_responses.py. 4 | 5 | 0123456789012345678901234567890123456789012345678901234567890123456789012345678 6 | 0000000000111111111122222222223333333333444444444455555555556666666666777777777 7 | 8 | # message_responses.py 9 | 10 | Don't change tabbing, spacing, or formating as the 11 | file is automatically linted and beautified. 12 | 13 | """ 14 | 15 | 16 | from chat_functions import send_text_to_room 17 | import logging 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | 22 | class Message(object): 23 | """Process messages.""" 24 | 25 | def __init__(self, client, store, config, message_content, room, event): 26 | """Initialize a new Message. 27 | 28 | Arguments: 29 | --------- 30 | client (nio.AsyncClient): nio client used to interact with matrix 31 | 32 | store (Storage): Bot storage 33 | 34 | config (Config): Bot configuration parameters 35 | 36 | message_content (str): The body of the message 37 | 38 | room (nio.rooms.MatrixRoom): The room the event came from 39 | 40 | event (nio.events.room_events.RoomMessageText): The event defining 41 | the message 42 | 43 | """ 44 | self.client = client 45 | self.store = store 46 | self.config = config 47 | self.message_content = message_content 48 | self.room = room 49 | self.event = event 50 | 51 | async def process(self): 52 | """Process and possibly respond to the message.""" 53 | if self.message_content.lower() == "hello world": 54 | await self._hello_world() 55 | 56 | async def _hello_world(self): 57 | """Say hello.""" 58 | text = "Hello, world!" 59 | await send_text_to_room(self.client, self.room.room_id, text) 60 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | matrix-nio>=0.8.0 2 | Markdown>=3.1.1 3 | PyYAML>=5.1.2 4 | Pillow 5 | python-magic 6 | python-olm 7 | cachetools 8 | atomicwrites 9 | peewee 10 | -------------------------------------------------------------------------------- /room_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 RoomDictSanityError(Exception): 11 | pass 12 | 13 | 14 | class RoomDict: 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 | DEFAULT_OPT_ARGS = None 25 | 26 | def __init__(self, room_dict_filepath): 27 | """Initialize room dictionary. 28 | 29 | Arguments: 30 | --------- 31 | room_dict (str): Path to room dictionary. 32 | 33 | """ 34 | 35 | self.room_dict = None 36 | self.rooms = {} 37 | 38 | self._last_matched_room = None 39 | 40 | self.load(room_dict_filepath) 41 | 42 | self.assert_sanity() 43 | 44 | return 45 | 46 | def __contains__(self, room): 47 | return room in self.rooms.keys() 48 | 49 | def __getitem__(self, item): 50 | return self.rooms[item] 51 | 52 | def __iter__(self): 53 | return self.rooms.__iter__() 54 | 55 | def load(self, room_dict_filepath): 56 | """Try loading the room dictionary. 57 | 58 | Arguments: 59 | ---------- 60 | room_dict_filepath (string): path to room dictionary. 61 | 62 | """ 63 | try: 64 | with open(room_dict_filepath) as fobj: 65 | logger.debug(f"Loading room dictionary at {room_dict_filepath}") 66 | self.room_dict = yaml.safe_load(fobj.read()) 67 | 68 | if "rooms" in self.room_dict.keys(): 69 | self.rooms = self.room_dict["rooms"] 70 | 71 | if "paths" in self.room_dict.keys(): 72 | os.environ["PATH"] = os.pathsep.join(self.room_dict["paths"]+[os.environ["PATH"]]) 73 | logger.debug(f'Path modified. Now: {os.environ["PATH"]}.') 74 | 75 | except FileNotFoundError: 76 | logger.error(f"File not found: {room_dict_filepath}") 77 | 78 | return 79 | 80 | def is_empty(self): 81 | """Returns whether there are rooms in the dictionary. 82 | 83 | """ 84 | return len(self.rooms) == 0 85 | 86 | def assert_sanity(self): 87 | """Raises a RoomDictSanityError exception if the room dictionary 88 | is not considered "sane". 89 | 90 | """ 91 | # Maybe in the future: Check whether rooms can be found in path 92 | # For now, let the OS handle this 93 | 94 | # Check whether room dictionary has a correct structure. Namely, 95 | # that: 96 | # 97 | # 1. Toplevel children may only be called "rooms" or "paths". 98 | if len(self.room_dict) > 2: 99 | raise RoomDictSanityError("Only two toplevel children allowed.") 100 | for key in self.room_dict.keys(): 101 | if key not in ("rooms","paths"): 102 | raise RoomDictSanityError( 103 | f"Invalid toplevel child found: {key}.") 104 | # 2. "paths" node must be a list, and must only contain string 105 | # children. 106 | if "paths" in self.room_dict: 107 | if type(self.room_dict["paths"]) != list: 108 | raise RoomDictSanityError( 109 | "The \"paths\" node must be a list.") 110 | for path in self.room_dict["paths"]: 111 | if type(path) != str: 112 | raise RoomDictSanityError("Defined paths must be strings.") 113 | # 3. "rooms" node chilren (henceforth room nodes) must be 114 | # dictionaries, 115 | # 4. and may contain only the following keys: 116 | # "regex", "cmd", "help", "markdown_convert", "formatted", 117 | # "code" and "split". 118 | # 5. The room node children may only be strings. 119 | # 6. Room node children with keys "markdown_convert", 120 | # "formatted" or "code" may only be defined as "true" or as 121 | # "false". 122 | if "rooms" in self.room_dict.keys(): 123 | for com in self.room_dict["rooms"]: 124 | # Implement rule 3 125 | if type(self.room_dict["rooms"][com]) != dict: 126 | raise RoomDictSanityError( 127 | "Defined rooms must be dictionaries.") 128 | for opt in self.room_dict["rooms"][com].keys(): 129 | # Implement rule 4 130 | if opt not in ("regex", 131 | "cmd", 132 | "args", 133 | "help", 134 | "markdown_convert", 135 | "formatted", 136 | "code", 137 | "split"): 138 | raise RoomDictSanityError( 139 | f"In room \"{com}\", invalid option found: " \ 140 | f"\"{opt}\".") 141 | # Implement rule 6 142 | elif opt in ("markdown_convert", "formatted", "code"): 143 | if type(self.room_dict["rooms"][com][opt]) != bool: 144 | raise RoomDictSanityError( 145 | f"In room \"{com}\", invalid value for option " 146 | f"\"{opt}\" found: " \ 147 | f"\"{self.room_dict['rooms'][com][opt]}\"") 148 | # Implement rule 5 149 | else: 150 | if type(self.room_dict["rooms"][com][opt]) != str: 151 | raise RoomDictSanityError( 152 | f"In room \"{com}\", room option " \ 153 | f"\"{opt}\" must be a string.") 154 | 155 | return 156 | 157 | def match(self, string): 158 | """Returns whether the given string matches any of the rooms' names 159 | regex patterns. 160 | 161 | Arguments: 162 | ---------- 163 | string (str): string to match 164 | 165 | """ 166 | matched = False 167 | cmd = None 168 | 169 | if string in self.rooms.keys(): 170 | matched = True 171 | cmd = string 172 | 173 | else: 174 | for room in self.rooms.keys(): 175 | if "regex" in self.rooms[room].keys() \ 176 | and re.match(self.rooms[room]["regex"], string): 177 | matched = True 178 | cmd = room 179 | break 180 | 181 | self._last_matched_room = cmd 182 | 183 | return matched 184 | 185 | def get_last_matched_room(self): 186 | return self._last_matched_room 187 | 188 | def get_cmd(self, room): 189 | """Return the name of the executable associated with the given room, 190 | for the system to call. 191 | 192 | Arguments: 193 | ---------- 194 | room (str): Name of the room in the room dictionary 195 | 196 | """ 197 | return self.rooms[room]["cmd"] 198 | 199 | def get_help(self,room): 200 | """Return the help string of the given room. 201 | 202 | Arguments: 203 | ---------- 204 | room (str): Name of the room in the room dictionary 205 | 206 | """ 207 | if "help" in self.rooms[room]: 208 | return self.rooms[room]["help"] 209 | else: 210 | return "No help defined for this room." 211 | 212 | def get_opt_args(self, room): 213 | """Return value of the "args" option. 214 | 215 | Arguments: 216 | ---------- 217 | room (str): Name of the room in the room dictionary 218 | 219 | """ 220 | if "args" in self.room_dict["rooms"][room].keys(): 221 | return self.room_dict["rooms"][room]["args"].split() 222 | else: 223 | return RoomDict.DEFAULT_OPT_ARGS 224 | 225 | def get_opt_markdown_convert(self, room): 226 | """Return boolean of the "markdown_convert" option. 227 | 228 | Arguments: 229 | ---------- 230 | room (str): Name of the room in the room dictionary 231 | 232 | """ 233 | if "markdown_convert" in self.room_dict["rooms"][room].keys(): 234 | return self.room_dict["rooms"][room]["markdown_convert"] == "true" 235 | else: 236 | return RoomDict.DEFAULT_OPT_MARKDOWN_CONVERT 237 | 238 | def get_opt_formatted(self, room): 239 | """Return boolean of the "formatted" option. 240 | 241 | Arguments: 242 | ---------- 243 | room (str): Name of the room in the room dictionary 244 | 245 | """ 246 | if "formatted" in self.room_dict["rooms"][room].keys(): 247 | return self.room_dict["rooms"][room]["formatted"] == "true" 248 | else: 249 | return RoomDict.DEFAULT_OPT_FORMATTED 250 | 251 | def get_opt_code(self, room): 252 | """Return boolean of the "code" option. 253 | 254 | Arguments: 255 | ---------- 256 | room (str): Name of the room in the room dictionary 257 | 258 | """ 259 | if "code" in self.room_dict["rooms"][room].keys(): 260 | return self.room_dict["rooms"][room]["code"] == "true" 261 | else: 262 | return RoomDict.DEFAULT_OPT_CODE 263 | 264 | def get_opt_split(self, room): 265 | """Return the string defined in the "split" option, or None. 266 | 267 | Arguments: 268 | ---------- 269 | room (str): Name of the room in the room dictionary 270 | 271 | """ 272 | if "split" in self.room_dict["rooms"][room].keys(): 273 | return self.room_dict["rooms"][room]["split"] 274 | else: 275 | return RoomDict.DEFAULT_OPT_SPLIT 276 | 277 | -------------------------------------------------------------------------------- /storage.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import os.path 3 | import logging 4 | 5 | latest_db_version = 0 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class Storage(object): 11 | def __init__(self, db_path): 12 | """Setup the database 13 | 14 | Runs an initial setup or migrations depending on whether a database file has already 15 | been created 16 | 17 | Args: 18 | db_path (str): The name of the database file 19 | """ 20 | self.db_path = db_path 21 | 22 | # Check if a database has already been connected 23 | if os.path.isfile(self.db_path): 24 | self._run_migrations() 25 | else: 26 | self._initial_setup() 27 | 28 | def _initial_setup(self): 29 | """Initial setup of the database""" 30 | logger.info("Performing initial database setup...") 31 | 32 | # Initialize a connection to the database 33 | self.conn = sqlite3.connect(self.db_path) 34 | self.cursor = self.conn.cursor() 35 | 36 | # Sync token table 37 | self.cursor.execute("CREATE TABLE sync_token (" 38 | "dedupe_id INTEGER PRIMARY KEY, " 39 | "token TEXT NOT NULL" 40 | ")") 41 | 42 | logger.info("Database setup complete") 43 | 44 | def _run_migrations(self): 45 | """Execute database migrations""" 46 | # Initialize a connection to the database 47 | self.conn = sqlite3.connect(self.db_path) 48 | self.cursor = self.conn.cursor() 49 | --------------------------------------------------------------------------------