@ -0,0 +1,13 @@ | |||
**/*.pyc | |||
**/__pycache__ | |||
**/__pycache__/** | |||
virtual_env | |||
virtual_env/** | |||
.vscode | |||
.vscode/** | |||
.idea | |||
.idea/** | |||
.atom | |||
.atom/** | |||
/telegrambot.txt | |||
/db.sqlite3 |
@ -0,0 +1,4 @@ | |||
deploy: | |||
stage: deploy | |||
script: | |||
make deploy |
@ -0,0 +1,44 @@ | |||
help: | |||
@echo -n | |||
deploy: | |||
-sudo systemctl stop telegram-bot-use-proper-grammar.service | |||
-sudo rm /etc/systemd/system/telegram-bot-use-proper-grammar.service | |||
-sudo install telegram-bot-use-proper-grammar.service /etc/systemd/system | |||
-sudo mkdir -p /var/www/telegram-bot-use-proper-grammar | |||
-sudo rm -rf /var/www/telegram-bot-use-proper-grammar/propergrammar | |||
-sudo rm -rf /var/www/telegram-bot-use-proper-grammar/Makefile | |||
-sudo rm -rf /var/www/telegram-bot-use-proper-grammar/manage.py | |||
-sudo cp -R propergrammar /var/www/telegram-bot-use-proper-grammar/propergrammar | |||
-sudo install manage.py /var/www/telegram-bot-use-proper-grammar | |||
-sudo install Makefile /var/www/telegram-bot-use-proper-grammar | |||
-sudo install requirements.frozen.txt /var/www/telegram-bot-use-proper-grammar | |||
sudo make depends -C /var/www/telegram-bot-use-proper-grammar | |||
sudo make migrate -C /var/www/telegram-bot-use-proper-grammar | |||
cd /var/www/telegram-bot-use-proper-grammar; sudo chown http:http -R . | |||
sudo systemctl daemon-reload | |||
sudo systemctl enable telegram-bot-use-proper-grammar.service | |||
sudo systemctl restart telegram-bot-use-proper-grammar.service | |||
devmigrate: virtual_env | |||
. virtual_env/bin/activate; python manage.py makemigrations | |||
. virtual_env/bin/activate; python manage.py migrate | |||
migrate: virtual_env | |||
. virtual_env/bin/activate; python manage.py migrate | |||
serve: virtual_env | |||
. virtual_env/bin/activate; python -m propergrammar | |||
depends: virtual_env | |||
. virtual_env/bin/activate; pip install -U -r requirements.frozen.txt | |||
depends-latest: virtual_env | |||
. virtual_env/bin/activate; pip install -U -r requirements.txt | |||
freeze: virtual_env | |||
. virtual_env/bin/activate; python -m pip freeze > requirements.frozen.txt | |||
virtual_env: | |||
python3 -m virtualenv virtual_env | |||
make depends |
@ -0,0 +1,17 @@ | |||
Telegram - Use Proper Grammar, Please! | |||
=========================== | |||
Sometimes we need a bot that repeats a set of messages over and over... even more than the frequency you're willing to copy-paste. | |||
Because of those people who need to be reminded more than you're able to remind them, this bot exists. | |||
## Running locally | |||
- Create a bot | |||
- Create the file `telegrambot.txt` containing the API key | |||
- Run `make serve` on a terminal | |||
## Deploying | |||
- Copy `telegrambot.txt` to `/var/www/telegram-bot-use-proper-grammar` | |||
- Run `make deploy` |
@ -0,0 +1,11 @@ | |||
#!/usr/bin/env python3 | |||
# -*- encoding: utf-8 -*- | |||
import sys | |||
from propergrammar.djangosetup import setup_django | |||
if __name__ == '__main__': | |||
setup_django() | |||
from django.core.management import execute_from_command_line | |||
execute_from_command_line(sys.argv) |
@ -0,0 +1,9 @@ | |||
#!/usr/bin/env python3 | |||
# -*- encoding: utf-8 -*- | |||
from pathlib import Path | |||
from .telebot import start_bot | |||
def main(): | |||
start_bot(Path('telegrambot.txt').read_text().strip()) |
@ -0,0 +1,9 @@ | |||
#!/usr/bin/env python3 | |||
# -*- encoding: utf-8 -*- | |||
from . import main | |||
from .djangosetup import setup_django | |||
if __name__ == "__main__": | |||
setup_django() | |||
main() |
@ -0,0 +1,13 @@ | |||
#!/usr/bin/env python3 | |||
# -*- encoding: utf-8 -*- | |||
import os, sys | |||
def setup_django(): | |||
# Setup environ | |||
sys.path.append(os.getcwd()) | |||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "propergrammar.settings") | |||
# Setup django | |||
import django | |||
django.setup() |
@ -0,0 +1,47 @@ | |||
# Generated by Django 3.0.3 on 2020-02-15 23:15 | |||
from django.db import migrations, models | |||
import django.db.models.deletion | |||
class Migration(migrations.Migration): | |||
initial = True | |||
dependencies = [ | |||
] | |||
operations = [ | |||
migrations.CreateModel( | |||
name='Group', | |||
fields=[ | |||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | |||
('chat_id', models.IntegerField(default=0, unique=True)), | |||
('name', models.CharField(default='', max_length=255)), | |||
], | |||
), | |||
migrations.CreateModel( | |||
name='GroupDictionaryEntry', | |||
fields=[ | |||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | |||
('word', models.TextField(default='')), | |||
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='words', to='propergrammar.Group')), | |||
], | |||
), | |||
migrations.AddIndex( | |||
model_name='group', | |||
index=models.Index(fields=['id'], name='propergramm_id_f9b41c_idx'), | |||
), | |||
migrations.AddIndex( | |||
model_name='group', | |||
index=models.Index(fields=['chat_id'], name='propergramm_chat_id_c6909f_idx'), | |||
), | |||
migrations.AddIndex( | |||
model_name='groupdictionaryentry', | |||
index=models.Index(fields=['id'], name='propergramm_id_1fdb42_idx'), | |||
), | |||
migrations.AddIndex( | |||
model_name='groupdictionaryentry', | |||
index=models.Index(fields=['group_id'], name='propergramm_group_i_fd6c71_idx'), | |||
), | |||
] |
@ -0,0 +1,66 @@ | |||
#!/usr/bin/env python3 | |||
# -*- encoding: utf-8 -*- | |||
from django.db import models | |||
VALID_LANGUAGES_LST = 'en,es,de,fr,pt'.split(',') | |||
VALID_LANGUAGES = tuple(zip( | |||
*(VALID_LANGUAGES_LST, 'English,Español,Deutsch,Français,Português'.split(',')) | |||
)) | |||
VALID_LANGUAGES_DICT = dict(VALID_LANGUAGES) | |||
LANGUAGES_SCOLDING = ''' | |||
I guessed you were writing in "{0}" and I have the following suggestions: | |||
Supuse que estabas escribiendo en "{0}" y tengo las siguientes sugerencias: | |||
Ich vermute, Sie haben auf "{0}" geschrieben und ich habe die folgenden Vorschläge: | |||
J'ai deviné que vous écriviez en "{0}" et j'ai les suggestions suivantes: | |||
Imaginei que você estivesse escrevendo em "{0}" e tenho as seguintes sugestões:'''.splitlines()[1:] | |||
LANGUAGES_SCOLDING_DICT = dict(tuple(zip( | |||
*(VALID_LANGUAGES_LST, LANGUAGES_SCOLDING) | |||
))) | |||
class Group(models.Model): | |||
chat_id = models.IntegerField( | |||
default=0, | |||
blank=False, | |||
null=False, | |||
unique=True | |||
) | |||
name = models.CharField( | |||
default="", | |||
blank=False, | |||
null=False, | |||
max_length=255 | |||
) | |||
def __str__(self): | |||
return f'{self.pk} - {self.name}' | |||
class Meta: | |||
indexes = [ | |||
models.Index(fields=['id']), | |||
models.Index(fields=['chat_id']), | |||
] | |||
class GroupDictionaryEntry(models.Model): | |||
group = models.ForeignKey( | |||
Group, | |||
on_delete=models.CASCADE, | |||
related_name='words' | |||
) | |||
word = models.TextField( | |||
default="", | |||
blank=False, | |||
null=False | |||
) | |||
def __str__(self): | |||
return f'{self.pk} - {self.word}' | |||
class Meta: | |||
indexes = [ | |||
models.Index(fields=['id']), | |||
models.Index(fields=['group_id']), | |||
] |
@ -0,0 +1,42 @@ | |||
#!/usr/bin/env python3 | |||
# -*- encoding: utf-8 -*- | |||
import time | |||
class MWT(object): | |||
"""Memoize With Timeout""" | |||
_caches = {} | |||
_timeouts = {} | |||
def __init__(self, timeout=2): | |||
self.timeout = timeout | |||
def collect(self): | |||
"""Clear cache of results which have timed out""" | |||
for func in self._caches: | |||
cache = {} | |||
for key in self._caches[func]: | |||
if (time.time() - self._caches[func][key][1]) < self._timeouts[func]: | |||
cache[key] = self._caches[func][key] | |||
self._caches[func] = cache | |||
def __call__(self, f): | |||
self.cache = self._caches[f] = {} | |||
self._timeouts[f] = self.timeout | |||
def func(*args, **kwargs): | |||
kw = sorted(kwargs.items()) | |||
key = (args, tuple(kw)) | |||
try: | |||
v = self.cache[key] | |||
# print("cache") | |||
if (time.time() - v[1]) > self.timeout: | |||
raise KeyError | |||
except KeyError: | |||
# print("new") | |||
v = self.cache[key] = f(*args, **kwargs), time.time() | |||
return v[0] | |||
func.func_name = f.__name__ | |||
return func |
@ -0,0 +1,19 @@ | |||
#!/usr/bin/env python3 | |||
# -*- encoding: utf-8 -*- | |||
SECRET_KEY = "plz, stop complaining" | |||
INSTALLED_APPS = [ | |||
'propergrammar' | |||
] | |||
DATABASES = { | |||
'default': { | |||
'ENGINE': 'django.db.backends.sqlite3', | |||
'NAME': 'db.sqlite3', | |||
} | |||
} | |||
USE_TZ = True | |||
TIME_ZONE = "UTC" |
@ -0,0 +1,308 @@ | |||
#!/usr/bin/env python3 | |||
# -*- encoding: utf-8 -*- | |||
from .mwt import MWT | |||
import sys | |||
import telegram | |||
import telegram.ext | |||
from telegram.ext import Updater | |||
from telegram.ext import CommandHandler | |||
from telegram.ext import MessageHandler | |||
from telegram.ext.filters import Filters | |||
from spellchecker import SpellChecker | |||
import logging | |||
from django.utils import timezone | |||
import random | |||
import datetime | |||
from io import BytesIO, StringIO | |||
from telegram.constants import MAX_MESSAGE_LENGTH | |||
CHECKERS = dict() | |||
Group: 'propergrammar.models.Group' = None | |||
GroupDictionaryEntry: 'propergrammar.models.GroupDictionaryEntry' = None | |||
logger = None | |||
models = None | |||
@MWT(timeout=60*5) | |||
def get_admin_ids(bot, chat_id): | |||
"""Returns a list of admin IDs for a given chat. Results are cached for 1 hour.""" | |||
return [admin.user.id for admin in bot.get_chat_administrators(chat_id)] | |||
def start(update: telegram.Update, context: telegram.ext.CallbackContext): | |||
chattype = update.message.chat.type | |||
if chattype in ['private']: | |||
context.bot.send_message( | |||
chat_id=update.message.chat_id, | |||
text="Invite me to your server!" | |||
) | |||
elif chattype in ['group', 'supergroup']: | |||
admins = get_admin_ids(context.bot, update.message.chat_id) | |||
if update.message.from_user.id in admins: | |||
is_group_admin(update, context) | |||
send_help_message(update, context) | |||
def send_help_message(update: telegram.Update, context: telegram.ext.CallbackContext): | |||
context.bot.send_message( | |||
chat_id=update.message.chat_id, | |||
text='''Try using my services with the commands: | |||
/add_group_dictionary | |||
/show_group_dictionary | |||
/remove_group_dictionary | |||
/export_group_dictionary | |||
(there is a clear_group_dictionary, but you'll have to type it yourself)''' | |||
) | |||
def split_long_messages(message: str) -> list: | |||
if len(message) <= MAX_MESSAGE_LENGTH: | |||
return [message] | |||
else: | |||
lst = [] | |||
lines = message.splitlines() | |||
size = 0 | |||
buffer = '' | |||
for line in lines: | |||
if len(buffer) + len(line) + 1 <= MAX_MESSAGE_LENGTH: | |||
buffer += line + '\n' | |||
else: | |||
lst.append(buffer) | |||
buffer = '' | |||
if len(line) + 1 <= MAX_MESSAGE_LENGTH: | |||
buffer += line + '\n' | |||
else: | |||
remainder = line | |||
while len(remainder) + 1 >= MAX_MESSAGE_LENGTH: | |||
humongous, remainder = ( | |||
remainder[:MAX_MESSAGE_LENGTH], remainder[MAX_MESSAGE_LENGTH:]) | |||
lst.append(humongous) | |||
lst.append(remainder) | |||
buffer = '' | |||
if len(buffer) > 0: | |||
lst.append(buffer) | |||
return lst | |||
def get_group(chat): | |||
group = Group.objects.filter(chat_id=chat.id).first() | |||
title = chat.title | |||
if title is None: | |||
title = f'{chat.first_name} {chat.last_name}' | |||
if group is None: | |||
group = Group(chat_id=chat.id, name=title) | |||
group.save() | |||
elif group.name != title: | |||
group.name = title | |||
group.save() | |||
return group | |||
def cmd_null(update: telegram.Update, context: telegram.ext.CallbackContext): | |||
return | |||
def remove_links(text, entities): | |||
if isinstance(text, str): | |||
text = list(text) | |||
for entity in entities: | |||
if entity.type in ['url', 'email', 'bot_command']: | |||
for i in range(entity.offset, entity.offset+entity.length): | |||
text[i] = '' | |||
return ''.join(text) | |||
else: | |||
return text | |||
def handle_message(update: telegram.Update, context: telegram.ext.CallbackContext): | |||
chattype = update.message.chat.type | |||
if chattype in ['group', 'supergroup', 'private']: | |||
to_check = ' '.join( | |||
list(filter(lambda x: x is not None, [ | |||
remove_links(update.message.text, update.message.entities), | |||
remove_links(update.message.caption, | |||
update.message.caption_entities) | |||
])) | |||
).strip() | |||
if len(to_check) > 0: | |||
group = get_group(update.message.chat) | |||
languages = models.VALID_LANGUAGES_LST | |||
ignored = [x.word for x in group.words.all()] | |||
language_rank = list() | |||
for language in languages: | |||
chk = CHECKERS[language] | |||
words = chk.split_words(to_check) | |||
if len(words) > 0: | |||
language_rank.append( | |||
(len(chk.known(words))/len(words), language) | |||
) | |||
language_rank.sort() | |||
language_rank.reverse() | |||
language_confidence, language_main = language_rank[0] | |||
if language_confidence == 0: | |||
language_main = 'en' | |||
checker_main = CHECKERS[language_main] | |||
words = checker_main.split_words(to_check) | |||
unknown = checker_main.unknown(words) | |||
for lng in languages: | |||
unknown = CHECKERS[lng].unknown(unknown) | |||
unknown = set(unknown) | |||
for unknown_word in list(unknown): | |||
for ignored_word in ignored: | |||
if unknown_word.lower() == ignored_word.lower(): | |||
unknown.difference_update(unknown_word) | |||
unknown = sorted(list(unknown)) | |||
formatted_suggestions = [] | |||
for typo in unknown: | |||
typo_fixed = checker_main.correction(typo) | |||
typo_fixes = checker_main.candidates(typo) | |||
typo_fixes -= {typo_fixed} | |||
formatted_suggestions.append( | |||
f'{typo} → {typo_fixed}; {", ".join(typo_fixes)}' | |||
) | |||
if len(unknown) > 0: | |||
lecture = models.LANGUAGES_SCOLDING_DICT[language_main].format( | |||
models.VALID_LANGUAGES_DICT[language_main] | |||
)+'\n'+"\n".join(formatted_suggestions) | |||
for message_segments in split_long_messages(lecture): | |||
context.bot.send_message( | |||
chat_id=update.message.chat_id, | |||
reply_to_message_id=update.message.message_id, | |||
text=message_segments | |||
) | |||
def cmd_agd(update: telegram.Update, context: telegram.ext.CallbackContext): | |||
chattype = update.message.chat.type | |||
if chattype in ['group', 'supergroup']: | |||
admins = get_admin_ids(context.bot, update.message.chat_id) | |||
if update.message.from_user.id in admins: | |||
group = get_group(update.message.chat) | |||
wordlist = sorted( | |||
list(set([w.word.lower() for w in group.words.all()]))) | |||
new_words = ' '.join(remove_links( | |||
update.message.text, update.message.entities).splitlines()).split() | |||
status = f'Adding {len(new_words)} new words...\n' | |||
for nword in new_words: | |||
status += f'{nword}... ' | |||
if nword.lower() in wordlist: | |||
status += 'already on list\n' | |||
else: | |||
GroupDictionaryEntry(group=group, word=nword).save() | |||
wordlist.append(nword.lower()) | |||
status += 'OK\n' | |||
status += f'New word count: {len(group.words.all())}' | |||
for segment in split_long_messages(status): | |||
context.bot.send_message( | |||
chat_id=update.message.chat_id, | |||
reply_to_message_id=update.message.message_id, | |||
text=segment | |||
) | |||
def cmd_rgd(update: telegram.Update, context: telegram.ext.CallbackContext): | |||
chattype = update.message.chat.type | |||
if chattype in ['group', 'supergroup']: | |||
admins = get_admin_ids(context.bot, update.message.chat_id) | |||
if update.message.from_user.id in admins: | |||
group = get_group(update.message.chat) | |||
old_words = ' '.join(remove_links( | |||
update.message.text, update.message.entities).splitlines()).split() | |||
status = f'Removing {len(old_words)} words...\n' | |||
for oword in old_words: | |||
status += f'{oword}... ' | |||
entry = group.words.all().filter(word__iexact=oword).first() | |||
if entry is None: | |||
status += 'not found\n' | |||
else: | |||
entry.delete() | |||
status += 'REMOVED\n' | |||
status += f'New word count: {len(group.words.all())}' | |||
for segment in split_long_messages(status): | |||
context.bot.send_message( | |||
chat_id=update.message.chat_id, | |||
reply_to_message_id=update.message.message_id, | |||
text=segment | |||
) | |||
def cmd_sgd(update: telegram.Update, context: telegram.ext.CallbackContext): | |||
chattype = update.message.chat.type | |||
if chattype in ['group', 'supergroup']: | |||
admins = get_admin_ids(context.bot, update.message.chat_id) | |||
if update.message.from_user.id in admins: | |||
group = get_group(update.message.chat) | |||
wordlist = sorted([w.word for w in group.words.all()]) | |||
wordlist_str = '\n'.join(wordlist) | |||
msg = 'Here are all your %d entries you have on your group\'s dictionary:\n%s' % ( | |||
len(wordlist), wordlist_str | |||
) | |||
for fragment in split_long_messages(msg): | |||
context.bot.send_message( | |||
chat_id=update.message.chat_id, | |||
reply_to_message_id=update.message.message_id, | |||
text=fragment | |||
) | |||
def cmd_egd(update: telegram.Update, context: telegram.ext.CallbackContext): | |||
chattype = update.message.chat.type | |||
if chattype in ['group', 'supergroup']: | |||
admins = get_admin_ids(context.bot, update.message.chat_id) | |||
if update.message.from_user.id in admins: | |||
group = get_group(update.message.chat) | |||
wordlist = sorted([w.word for w in group.words.all()]) | |||
wordlist_str = '\n'.join(wordlist) | |||
wordlist_str += '\n' | |||
bio = BytesIO(wordlist_str.encode('UTF-8')) | |||
bio.name = f'DictExport_{update.message.chat_id}_{str(update.message.date).replace(" ", "_").replace(":", "-")}.txt' | |||
context.bot.send_document( | |||
chat_id=update.message.chat_id, | |||
reply_to_message_id=update.message.message_id, | |||
document=bio | |||
) | |||
def cmd_cgd(update: telegram.Update, context: telegram.ext.CallbackContext): | |||
chattype = update.message.chat.type | |||
if chattype in ['group', 'supergroup']: | |||
admins = get_admin_ids(context.bot, update.message.chat_id) | |||
if update.message.from_user.id in admins: | |||
group = get_group(update.message.chat) | |||
group.words.all().delete() | |||
context.bot.send_message( | |||
chat_id=update.message.chat_id, | |||
reply_to_message_id=update.message.message_id, | |||
text='Erased all entries from local dictionary.' | |||
) | |||
def start_bot(token): | |||
global Group | |||
global GroupDictionaryEntry | |||
global logger | |||
global CHECKERS | |||
global models | |||
from .models import Group | |||
from .models import GroupDictionaryEntry | |||
from . import models | |||
for lng in models.VALID_LANGUAGES_LST: | |||
CHECKERS[lng] = SpellChecker(lng) | |||
logging.basicConfig( | |||
level=logging.DEBUG, | |||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' | |||
) | |||
logger = logging.getLogger() | |||
logger.setLevel(logging.INFO) | |||
u = Updater(token, use_context=True) | |||
d = u.dispatcher | |||
d.add_handler(CommandHandler('start', start)) | |||
d.add_handler(CommandHandler('add_group_dictionary', cmd_agd)) | |||
d.add_handler(CommandHandler('remove_group_dictionary', cmd_rgd)) | |||
d.add_handler(CommandHandler('show_group_dictionary', cmd_sgd)) | |||
d.add_handler(CommandHandler('export_group_dictionary', cmd_egd)) | |||
d.add_handler(CommandHandler('clear_group_dictionary', cmd_cgd)) | |||
d.add_handler(CommandHandler('help', send_help_message)) | |||
d.add_handler(MessageHandler(Filters.all, handle_message)) | |||
u.start_polling() |
@ -0,0 +1,14 @@ | |||
asgiref==3.2.3 | |||
certifi==2019.11.28 | |||
cffi==1.14.0 | |||
cryptography==2.8 | |||
decorator==4.4.1 | |||
Django==3.0.3 | |||
future==0.18.2 | |||
pycparser==2.19 | |||
pyspellchecker==0.5.3 | |||
python-telegram-bot==12.4.2 | |||
pytz==2019.3 | |||
six==1.14.0 | |||
sqlparse==0.3.0 | |||
tornado==6.0.3 |
@ -0,0 +1,3 @@ | |||
django | |||
python-telegram-bot | |||
pyspellchecker |
@ -0,0 +1 @@ | |||
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><g fill="#696969" transform="matrix(1.1845524 0 0 1.1845524 1.815447 3.785371)"><path d="m10.000001 12h-1.4708747q-.2548545 0-.4077671-.120482-.1529126-.128011-.2257281-.316265l-.6116504-1.9653612h-3.5606793l-.604369 1.9578312q-.058253.165662-.2257281.308735-.1601941.135542-.4004854.135542h-1.4927183l3.5242715-10h1.9514562zm-5.847088-3.7876504h2.6941745l-.9830097-3.1626507q-.0873786-.2334337-.1820388-.5421687-.0946602-.3162652-.1893203-.6852411-.0873787.3689759-.1820388.6852411-.0946603.3162651-.1747574.5496987z"/><path d="m11.000003 11.896693v-9.896693h1.67105v3.8636362q.381578-.4132232.848682-.661157.467105-.2548209 1.052631-.2548209.55921 0 1.006577.2341597.44737.2341598.763158.6749311.315789.4338843.486841 1.053719.171053.6198347.171053 1.3911845 0 .8333333-.197369 1.5151514-.197368.681818-.559209 1.170798-.361842.482094-.86842.750689-.5.261708-1.118419.261708-.302632 0-.546052-.06198-.236842-.06198-.440789-.172176-.203947-.117079-.375001-.275482-.164473-.165289-.322367-.365013l-.07237.440771q-.0329.18595-.131579.261708-.09868.06887-.256579.06887h-1.111841zm2.993417-5.5853984q-.421053 0-.730263.1997244-.30921.1928376-.592104.5647383v3.0165287q.249999.323691.539473.454545.289473.130854.618419.130854.328948 0 .598684-.123966.269738-.130855.453948-.406337.190788-.2823686.289473-.7162529.105263-.4407713.105263-1.060606 0-.5509641-.08553-.9366391-.085523-.3925619-.249996-.6404958-.157894-.2479338-.401315-.3650138-.236843-.1170798-.546052-.1170798z"/><path d="m20.867188 5c-.469484 0-.881719.0898437-1.240235.2675781-.354246.1777345-.653206.4238281-.896484.7382813-.239011.309896-.421149.675781-.544922 1.0996094-.123773.4238283-.185547.8841143-.185547 1.3808593 0 .5559898.07009 1.0527342.210938 1.4902344.140845.4329425.331302.8007815.570312 1.1015625 1.357041 1.83064 3.119404.584038 4.17696-.147974l-.234157-1.0763632c-1.258632.4420062-1.788605 1.2671802-2.720146.2653532-.226207-.382813-.339845-.9264324-.339845-1.6328129 0-.3326824.0269-.6334634.07813-.9023437.05548-.2688804.135486-.4967447.242187-.6835938.106701-.186849.242112-.3313801.404297-.4316406.166453-.1002605.360093-.1503906.582031-.1503906.179257 0 .32617.026041.441406.076172.119506.05013.22128.1061199.306641.1699219.08963.059244.169993.1139319.238281.1640625.06829.050131.141563.076172.222657.076172.08535 0 .152265-.018229.199218-.054687.04694-.041016.09367-.094401.140625-.1582031l.429684-.6210946c-.268886-.3144533-.572982-.5527343-.910157-.7167969-.337174-.1686198-.727999-.2539062-1.171874-.2539062z"/></g><path d="m30.008379 19.015652c-.11263-.02069-.229582-.02189-.349814.0022-.256226.05414-.492352.195183-.658476.393444l-7.352978 8.22655-2.633903-2.575268c-.440541-.430791-1.242149-.430757-1.682772 0-.440564.430768-.440599 1.214497 0 1.645311l3.511871 3.433689.91455.858422.804804-.929957 8.194364-9.156507c.581205-.618654.04074-1.75309-.747646-1.897919z" fill="#76a797"/></svg> |
@ -0,0 +1,85 @@ | |||
<?xml version="1.0" encoding="UTF-8" standalone="no"?> | |||
<svg | |||
xmlns:dc="http://purl.org/dc/elements/1.1/" | |||
xmlns:cc="http://creativecommons.org/ns#" | |||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" | |||
xmlns:svg="http://www.w3.org/2000/svg" | |||
xmlns="http://www.w3.org/2000/svg" | |||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | |||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | |||
viewBox="0 0 48 48" | |||
version="1.1" | |||
id="svg12" | |||
sodipodi:docname="spelling_libreoffice_colibri_cc0_48.svg" | |||
width="48" | |||
height="48" | |||
inkscape:version="0.92.4 5da689c313, 2019-01-14" | |||
inkscape:export-filename="/home/sfner/Documents/telegram_bots/use_proper_grammar/spelling_libreoffice_colibri_cc0.svg.png" | |||
inkscape:export-xdpi="960" | |||
inkscape:export-ydpi="960"> | |||
<metadata | |||
id="metadata18"> | |||
<rdf:RDF> | |||
<cc:Work | |||
rdf:about=""> | |||
<dc:format>image/svg+xml</dc:format> | |||
<dc:type | |||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> | |||
<dc:title></dc:title> | |||
</cc:Work> | |||
</rdf:RDF> | |||
</metadata> | |||
<defs | |||
id="defs16" /> | |||
<sodipodi:namedview | |||
pagecolor="#ffffff" | |||
bordercolor="#666666" | |||
borderopacity="1" | |||
objecttolerance="10" | |||
gridtolerance="10" | |||
guidetolerance="10" | |||
inkscape:pageopacity="0" | |||
inkscape:pageshadow="2" | |||
inkscape:window-width="1920" | |||
inkscape:window-height="1027" | |||
id="namedview14" | |||
showgrid="false" | |||
inkscape:zoom="10.429825" | |||
inkscape:cx="16.414963" | |||
inkscape:cy="11.338944" | |||
inkscape:window-x="0" | |||
inkscape:window-y="27" | |||
inkscape:window-maximized="1" | |||
inkscape:current-layer="svg12" /> | |||
<rect | |||
style="fill:#ffffff;fill-opacity:0.97875815;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" | |||
id="rect847" | |||
width="48" | |||
height="48" | |||
x="0" | |||
y="0" | |||
inkscape:export-xdpi="960" | |||
inkscape:export-ydpi="960" /> | |||
<g | |||
transform="matrix(1.1845524,0,0,1.1845524,9.8154469,11.785371)" | |||
id="g8" | |||
style="fill:#696969"> | |||
<path | |||
d="M 10.000001,12 H 8.5291263 Q 8.2742718,12 8.1213592,11.879518 7.9684466,11.751507 7.8956311,11.563253 L 7.2839807,9.5978918 H 3.7233014 L 3.1189324,11.555723 Q 3.0606794,11.721385 2.8932043,11.864458 2.7330102,12 2.4927189,12 H 1.0000006 L 4.5242721,2 H 6.4757283 Z M 4.152913,8.2123496 H 6.8470875 L 5.8640778,5.0496989 Q 5.7766992,4.8162652 5.682039,4.5075302 5.5873788,4.191265 5.4927187,3.8222891 5.40534,4.191265 5.3106799,4.5075302 5.2160196,4.8237953 5.1359225,5.0572289 Z" | |||
id="path2" | |||
inkscape:connector-curvature="0" /> | |||
<path | |||
d="M 11.000003,11.896693 V 2 h 1.67105 v 3.8636362 q 0.381578,-0.4132232 0.848682,-0.661157 0.467105,-0.2548209 1.052631,-0.2548209 0.55921,0 1.006577,0.2341597 0.44737,0.2341598 0.763158,0.6749311 0.315789,0.4338843 0.486841,1.053719 0.171053,0.6198347 0.171053,1.3911845 0,0.8333333 -0.197369,1.5151514 -0.197368,0.681818 -0.559209,1.170798 -0.361842,0.482094 -0.86842,0.750689 -0.5,0.261708 -1.118419,0.261708 -0.302632,0 -0.546052,-0.06198 -0.236842,-0.06198 -0.440789,-0.172176 -0.203947,-0.117079 -0.375001,-0.275482 -0.164473,-0.165289 -0.322367,-0.365013 l -0.07237,0.440771 q -0.0329,0.18595 -0.131579,0.261708 -0.09868,0.06887 -0.256579,0.06887 H 11 Z M 13.99342,6.3112946 q -0.421053,0 -0.730263,0.1997244 -0.30921,0.1928376 -0.592104,0.5647383 v 3.0165287 q 0.249999,0.323691 0.539473,0.454545 0.289473,0.130854 0.618419,0.130854 0.328948,0 0.598684,-0.123966 0.269738,-0.130855 0.453948,-0.406337 0.190788,-0.2823686 0.289473,-0.7162529 0.105263,-0.4407713 0.105263,-1.060606 0,-0.5509641 -0.08553,-0.9366391 Q 15.10526,7.0413221 14.940787,6.7933882 14.782893,6.5454544 14.539472,6.4283744 14.302629,6.3112946 13.99342,6.3112946 Z" | |||
id="path4" | |||
inkscape:connector-curvature="0" /> | |||
<path | |||
d="M 20.867188,5 C 20.397704,5 19.985469,5.0898437 19.626953,5.2675781 19.272707,5.4453126 18.973747,5.6914062 18.730469,6.0058594 18.491458,6.3157554 18.30932,6.6816404 18.185547,7.1054688 18.061774,7.5292971 18,7.9895831 18,8.4863281 c 0,0.5559898 0.07009,1.0527342 0.210938,1.4902344 0.140845,0.4329425 0.331302,0.8007815 0.570312,1.1015625 1.357041,1.83064 3.119404,0.584038 4.17696,-0.147974 L 22.724053,9.8537878 C 21.465421,10.295794 20.935448,11.120968 20.003907,10.119141 19.7777,9.736328 19.664062,9.1927086 19.664062,8.4863281 c 0,-0.3326824 0.0269,-0.6334634 0.07813,-0.9023437 0.05548,-0.2688804 0.135486,-0.4967447 0.242187,-0.6835938 0.106701,-0.186849 0.242112,-0.3313801 0.404297,-0.4316406 0.166453,-0.1002605 0.360093,-0.1503906 0.582031,-0.1503906 0.179257,0 0.32617,0.026041 0.441406,0.076172 0.119506,0.05013 0.22128,0.1061199 0.306641,0.1699219 0.08963,0.059244 0.169993,0.1139319 0.238281,0.1640625 0.06829,0.050131 0.141563,0.076172 0.222657,0.076172 0.08535,0 0.152265,-0.018229 0.199218,-0.054687 0.04694,-0.041016 0.09367,-0.094401 0.140625,-0.1582031 L 22.949219,5.9707031 C 22.680333,5.6562498 22.376237,5.4179688 22.039062,5.2539062 21.701888,5.0852864 21.311063,5 20.867188,5 Z" | |||
id="path6" | |||
inkscape:connector-curvature="0" /> | |||
</g> | |||
<path | |||
d="m 38.008379,27.015652 c -0.11263,-0.02069 -0.229582,-0.02189 -0.349814,0.0022 -0.256226,0.05414 -0.492352,0.195183 -0.658476,0.393444 l -7.352978,8.22655 -2.633903,-2.575268 c -0.440541,-0.430791 -1.242149,-0.430757 -1.682772,0 -0.440564,0.430768 -0.440599,1.214497 0,1.645311 l 3.511871,3.433689 0.91455,0.858422 0.804804,-0.929957 8.194364,-9.156507 c 0.581205,-0.618654 0.04074,-1.75309 -0.747646,-1.897919 z" | |||
id="path10" | |||
inkscape:connector-curvature="0" | |||
style="fill:#76a797" /> | |||
</svg> |
@ -0,0 +1,13 @@ | |||
[Unit] | |||
Description=Use Proper Grammar Bot telegram service | |||
After=network.target | |||
[Service] | |||
User=http | |||
Group=http | |||
WorkingDirectory=/var/www/telegram_use_proper_grammar | |||
ExecStart=/usr/bin/make serve | |||
KillSignal=SIGINT | |||
[Install] | |||
WantedBy=multi-user.target |