diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fe33a8e --- /dev/null +++ b/.gitignore @@ -0,0 +1,152 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file diff --git a/README.md b/README.md index d222924..d5299b7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ # emeatech_bot Super hacky initial implementation of a Telegram bot to do tasks such as adding jobs and news to emeatech.org. + +# ENV VARIABLES + +EMEA_TELEGRAM_TOKEN = '' + +EMEATECH_AIRTABLE_API_KEY = '' diff --git a/db.py b/db.py new file mode 100644 index 0000000..26209b7 --- /dev/null +++ b/db.py @@ -0,0 +1,136 @@ +import json +import os +from abc import ABC, abstractmethod +from dataclasses import asdict, dataclass +from typing import Optional +from urllib import request, parse + + +@dataclass +class JobListing: + Description: str + URL: str + Company: Optional[str] = None + Position: Optional[str] = None + Salary: Optional[str] = None + + def from_txt(text_message): + """ + EXAMPLE: + >>> txt = 'Acme | rockstar ninja | $80k-$170k;Acme is a cool company that does cool things;https://example.com' + >>> JobListing.from_txt(txt) + JobListing(Description='Acme | rockstar ninja | $80k-$170k', URL='https://example.com', Company='Acme', Position='rockstar ninja', Salary='$80k-$170k') + """ + new_job_description, company_description, new_job_link = text_message.split(";") + company, position, salary = new_job_description.split(' | ') + return JobListing( + Description=new_job_description, + URL=new_job_link, + Company=company, + Position=position, + Salary=salary + ) + + def to_md(self)->str: + """ + EXAMPLE: + >>> job = JobListing(Description='Acme | rockstar ninja | $80k-$170k', URL='https://example.com', Company='Acme', Position='rockstar ninja', Salary='$80k-$180k') + >>> job.to_md() + '* **Acme | rockstar ninja | $80k-$180k.** Acme | rockstar ninja | $80k-$170k https://example.com' + """ + return f"* **{self.Company} | {self.Position} | {self.Salary}.** {self.Description} {self.URL}" + + + +class JobBoardDbBase(ABC): + """ + An abstract class for CRUD operations + """ + + @abstractmethod + def create(): + ... + @abstractmethod + def read(): + ... + @abstractmethod + def update(): + ... + @abstractmethod + def delete(): + ... + +class JobBoardDbAirTable(JobBoardDbBase): + """ + An AirTable manager + """ + def __init__(self): + self.key = os.getenv('EMEATECH_AIRTABLE_API_KEY') + if self.key is None: + raise Exception("Define the env variable EMEA_AIRTABLE_API") + self.url = "https://api.airtable.com/v0/appTgR7p7sKXHvip2/Table%201" + + def create(self, data:JobListing): + + body = {"records":[{"fields": asdict(data)}]} + + req = request.Request( + self.url, + method='POST', + data=json.dumps(body).encode(), + headers={ + 'Authorization': f'Bearer {self.key}', + 'Content-Type': 'application/json', + } + ) + + return json.loads(request.urlopen(req).read()) + + + def read(self): + try: + req = request.Request( + self.url, + headers={'Authorization': f'Bearer {self.key}'}, + method='GET' + ) + + return request.urlopen(req).read() + except: + raise Exception(resp) + + def update(): + raise NotImplementedError() + + def delete(self,row_id:str): + body = {'records[]':row_id} + req = request.Request( + f"{self.url}/?" + parse.urlencode(body), + method='DELETE', + headers={'Authorization': f'Bearer {self.key}'} + ) + with request.urlopen(req) as response: + return json.loads(response.read()) + + + +if __name__ == "__main__": + import doctest + doctest.testmod() + board = JobBoardDbAirTable() + # list all jobs + jobs = json.loads(board.read()) + for job in jobs['records']: + if job['fields']: + fields = JobListing(**job['fields']) + print(fields.to_md()) + + # create a new job + new_job = board.create( + JobListing(Description='You should drink coffee all day', Company='ACME', Position='ninja', Salary='$100k', URL='http://acme.com/recruiting') + ) + print(new_job) + # delete a job + deleted = board.delete(new_job['records'][0]['id']) + assert deleted['records'][0]['deleted'] == True + diff --git a/emeatechbot.py b/emeatechbot.py index 3cb6c78..1d0e666 100644 --- a/emeatechbot.py +++ b/emeatechbot.py @@ -1,9 +1,19 @@ +from db import JobListing +from pathlib import Path import os -from telegram.ext import Updater, CommandHandler, MessageHandler, Filters -import os + +from db import JobBoardDbAirTable +from telegram.ext import CommandHandler, Filters, MessageHandler, Updater TOKEN = os.getenv("EMEA_TELEGRAM_TOKEN") +news_file_path = Path('news.md') + +job_file_path = Path('jobs.md') + +board = JobBoardDbAirTable() + + def start(update, context): context.bot.send_message(chat_id=update.effective_chat.id, text="Hi! I respond by echoing messages. Give it a try!") @@ -11,9 +21,9 @@ def echo(update, context): text = update.message.text if text.startswith("n "): print("updating news") - with open("/home/tasks/emeatech.org/docs/news/index.md") as f: + with job_file_path.open('r') as f: existing = f.read() - with open("/home/tasks/emeatech.org/docs/news/index.md", "w") as f: + with job_file_path.open('w') as f: link = text[2:] bullet = f"* [{link}]({link})\n" replacetext = "" @@ -25,14 +35,14 @@ def echo(update, context): elif text.startswith("j "): try: - with open("/home/tasks/emeatech.org/docs/jobs/index.md") as f: + with job_file_path.open('r') as f: existing = f.read() - with open("/home/tasks/emeatech.org/docs/jobs/index.md", "w") as f: + with job_file_path.open('w') as f: try: - new_job = text[2:] - new_job_description, company_description, new_job_link = new_job.split(";") - existing = existing.replace("## Listings", f"## Listings\n\n * **{new_job_description}.** {company_description}. More information [here]({new_job_link}).") + new_job = JobListing.from_txt(text[2:]) + existing = existing.replace("## Listings", f"## Listings\n\n * **{new_job.Description}.** {new_job.Description}. More information [here]({new_job.URL}).") f.write(existing) + board.create(new_job) context.bot.send_message(chat_id=update.effective_chat.id, text="Posted your job. Thanks!") except Exception as e: print("Exception in writing new jobs") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d02065d --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +python-telegram-bot==13.11 \ No newline at end of file