1 # irgramd: IRC-Telegram gateway 2 # utils.py: Helper functions 3 # 4 # Copyright (c) 2019 Peter Bui <pbui@bx612.space> 5 # Copyright (c) 2020-2026 E. Bosch <presidev@AT@gmail.com> 6 # 7 # Use of this source code is governed by a MIT style license that 8 # can be found in the LICENSE file included in this project. 9 10 import itertools 11 import textwrap 12 import re 13 import datetime 14 import zoneinfo 15 import difflib 16 import logging 17 import random 18 19 # Constants 20 21 FILENAME_INVALID_CHARS = re.compile('[\0-\x1F/{}<>"\'\\|*&#%?\x7F]') 22 SIMPLE_URL = re.compile('http(|s)://[^ ]+') 23 24 from include import MAX_LINE 25 26 # Utilities 27 28 class command: 29 async def parse_command(self, line, nick): 30 command = line.partition(' ')[0].lower() 31 self.tmp_ircnick = nick 32 if command in self.commands.keys(): 33 handler, min_args, max_args, maxsplit = self.commands[command] 34 words = line.split(maxsplit=maxsplit)[1:] 35 num_words = len(words) 36 if num_words < min_args or num_words > max_args: 37 reply = ('Wrong number of arguments',) 38 else: 39 reply = await handler(*words) 40 else: 41 reply = ('Unknown command',) 42 43 return reply 44 45 class HELP: 46 desc = 1 47 brief = 2 48 49 class LOGL: 50 debug = False 51 52 def chunks(iterable, n, fillvalue=None): 53 ''' Return iterable consisting of a sequence of n-length chunks ''' 54 args = [iter(iterable)] * n 55 return itertools.zip_longest(*args, fillvalue=fillvalue) 56 57 def set_replace(set, item, new_item): 58 if item in set: 59 set.remove(item) 60 set.add(new_item) 61 62 def get_continued(items, mark, length): 63 # Add "continued" mark to lines, except last one 64 return (x + mark if n != length else x for n, x in enumerate(items, start=1)) 65 66 def split_lines(message): 67 messages_limited = [] 68 wr = textwrap.TextWrapper(width=MAX_LINE) 69 70 # Split when Telegram original message has breaks 71 messages = message.splitlines() 72 lm = len(messages) 73 if lm > 1: 74 # Add "continued line" mark (\) for lines that belong to the same message 75 # (split previously) 76 messages = get_continued(messages, ' \\', lm) 77 for m in messages: 78 wrapped = wr.wrap(text=m) 79 lw = len(wrapped) 80 if lw > 1: 81 # Add double "continued line" mark (\\) for lines that belong to the same message 82 # and have been wrapped to not exceed IRC limits 83 messages_limited += get_continued(wrapped, ' \\\\', lw) 84 else: 85 messages_limited += wrapped 86 del wr 87 return messages_limited 88 89 def sanitize_filename(fn): 90 def hexize(m): 91 return '-{:x}-'.format(ord(m.group(0))) 92 93 new_fn = FILENAME_INVALID_CHARS.sub(hexize, fn) 94 return new_fn.lstrip('-').replace(' ','_') 95 96 def add_filename(filename, add): 97 if add: 98 aux = filename.rsplit('.', 1) 99 name = aux[0] 100 last_dot = '.' 101 try: 102 ext = aux[1] 103 except: 104 ext = '' 105 last_dot = '' 106 return '{}-{}{}{}'.format(name, add, last_dot, ext) 107 else: 108 return filename 109 110 def remove_slash(url): 111 return url[:-1] if url[-1:] == '/' else url 112 113 def remove_http_s(url): 114 if url[:8] == 'https://': 115 surl = url[8:] 116 elif url[:7] == 'http://': 117 surl = url[7:] 118 else: 119 surl = url 120 return remove_slash(surl) 121 122 def is_url_equiv(url1, url2): 123 if url1 and url2: 124 return url1 == url2 or remove_slash(remove_http_s(url1)) == remove_slash(remove_http_s(url2)) 125 else: 126 return False 127 128 def extract_url(text): 129 url = SIMPLE_URL.search(text) 130 return url.group() if url else None 131 132 def get_human_size(size): 133 human_units = ('', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') 134 135 def get_human_size_values(size, unit_pos=0): 136 aux = size / 1024.0 137 if aux > 1: return get_human_size_values(aux, unit_pos + 1) 138 else: return size, human_units[unit_pos] 139 140 if size <= 1237940039285380274899124224: # 1024Y 141 num, unit = get_human_size_values(size) 142 else: 143 num = size / 1208925819614629174706176 # 1Y 144 unit = 'Y' 145 146 fs = '{:.1f}{}' if num < 10 else '{:.0f}{}' 147 148 return fs.format(num, unit) 149 150 def get_human_duration(duration): 151 res = '' 152 x, s = divmod(duration, 60) 153 h, m = divmod(x, 60) 154 155 if h > 0: res = str(h) + 'h' 156 if m > 0: res += str(m) + 'm' 157 if s > 0 or duration < 60: res += str(s) + 's' 158 return res 159 160 def compact_date(date, tz): 161 delta = current_date() - date 162 date_local = date.astimezone(zoneinfo.ZoneInfo(tz)) 163 164 if delta.days < 1: 165 compact_date = date_local.strftime('%H:%M') 166 elif delta.days < 365: 167 compact_date = date_local.strftime('%d-%b') 168 else: 169 compact_date = date_local.strftime('%Y') 170 171 return compact_date 172 173 def current_date(): 174 return datetime.datetime.now(datetime.timezone.utc) 175 176 def get_highlighted(a, b): 177 awl = len(a.split()) 178 bwl = len(b.split()) 179 delta_size = abs(awl - bwl) 180 highlighted = True 181 182 if not a: 183 res = '> {}'.format(b) 184 elif delta_size > 5: 185 res = b 186 highlighted = False 187 else: 188 al = a.split(' ') 189 bl = b.split(' ') 190 diff = difflib.ndiff(al, bl) 191 ld = list(diff) 192 res = '' 193 d = '' 194 eq = 0 195 196 for i in ld: 197 if i == '- ' or i[0] == '?': 198 continue 199 elif i == ' ' or i == '+ ': 200 res += ' ' 201 continue 202 # deletion of words 203 elif i[0] == '-': 204 res += '-{}- '.format(i[2:]) 205 # addition of words 206 elif i[0] == '+': 207 res += '+{}+ '.format(i[2:]) 208 else: 209 res += '{} '.format(i[2:]) 210 eq += 1 211 212 delta_eq = bwl - eq 213 if delta_eq > 3: 214 res = b 215 highlighted = False 216 217 return res, highlighted 218 219 def fix_braces(text): 220 # Remove braces not closed, if the text was truncated 221 if text.endswith(' {...'): 222 subtext = text[:-5] 223 if not '{}' in subtext: 224 return '{}...'.format(subtext) 225 return text 226 227 def format_timestamp(format, tz, date): 228 date_local = date.astimezone(zoneinfo.ZoneInfo(tz)) 229 return date_local.strftime(format) 230 231 def parse_loglevel(level): 232 levelu = level.upper() 233 if levelu == 'DEBUG': 234 LOGL.debug = True 235 if levelu == 'NONE': 236 l = None 237 elif levelu in ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'): 238 l = getattr(logging, levelu) 239 else: 240 l = False 241 return l 242 243 def pretty(object): 244 return object.stringify() if LOGL.debug and object else object 245 246 class token: 247 def __init__(self, alpha): 248 self.alpha = alpha 249 self.long_alpha = len(alpha) 250 251 def gen_token(self, long_token): 252 if long_token == 1: 253 return self.alpha[random.randrange(self.long_alpha)] 254 else: 255 aux = self.gen_token(long_token - 1) 256 return aux + self.alpha[random.randrange(self.long_alpha)]