├── .prettierrc ├── gmt ├── views │ ├── __init__.py │ ├── api.py │ ├── articles.py │ ├── admin.py │ └── general.py ├── templates │ ├── general │ │ ├── robots.txt │ │ ├── sitemap.xml │ │ ├── contribute.html │ │ ├── privacy_policy.html │ │ ├── tos.html │ │ ├── credits.html │ │ ├── morning.html │ │ └── contact.html │ ├── admin │ │ └── index.html │ ├── auth │ │ ├── success.html │ │ ├── unsubscribe.html │ │ └── confirm.html │ ├── 404.html │ ├── writers │ │ ├── login.html │ │ ├── guidelines.html │ │ ├── portal.html │ │ ├── settings.html │ │ ├── apply.html │ │ └── register.html │ └── articles │ │ └── article.html ├── static │ ├── Meta-Images │ │ └── Landing Card.png │ ├── JavaScript │ │ ├── landing-page │ │ │ ├── scroller.js │ │ │ └── typewriter.js │ │ ├── writers │ │ │ ├── filter.js │ │ │ ├── create-articles.js │ │ │ ├── user-apply.js │ │ │ └── create-profile.js │ │ ├── layout │ │ │ ├── navbar.js │ │ │ └── darkmode.js │ │ └── signup │ │ │ └── signup.js │ ├── loader.js │ ├── config.css │ └── loader.css ├── news.py ├── utils.py ├── __init__.py └── extras.py ├── .vercelignore ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml ├── workflows │ ├── lint.yaml │ ├── black.yml │ ├── cron.yaml │ ├── articles.yaml │ └── codeql.yml └── dependabot.yml ├── pyproject.toml ├── requirements-dev.txt ├── nginx ├── Dockerfile └── nginx.conf ├── index.py ├── .gitignore ├── vercel.json ├── .pre-commit-config.yaml ├── docker-compose.yml ├── requirements.txt ├── rss.json ├── Dockerfile ├── LICENSE ├── config.template.py ├── package.json ├── tailwind.config.js ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md └── README.md /.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /gmt/views/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.vercelignore: -------------------------------------------------------------------------------- 1 | instance/ 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: goodmorningtech 2 | -------------------------------------------------------------------------------- /gmt/templates/general/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.isort] 2 | profile = "black" 3 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | black==24.4.2 2 | djlint==1.36.4 3 | pre-commit==4.2.0 4 | isort==5.13.2 5 | -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.25 2 | 3 | RUN rm /etc/nginx/conf.d/default.conf 4 | COPY nginx.conf /etc/nginx/conf.d -------------------------------------------------------------------------------- /index.py: -------------------------------------------------------------------------------- 1 | from gmt import create_app 2 | 3 | app = create_app() 4 | 5 | if __name__ == "__main__": 6 | app.run() 7 | -------------------------------------------------------------------------------- /gmt/static/Meta-Images/Landing Card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodMorninTech/GoodMorningTech/HEAD/gmt/static/Meta-Images/Landing Card.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | 3 | *.pyc 4 | __pycache__/ 5 | 6 | instance/ 7 | 8 | dist/ 9 | build/ 10 | *.egg-info/ 11 | 12 | .idea/ 13 | 14 | node_modules/ 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a question 4 | about: Join our discord server to ask questions 5 | url: https://discord.goodmorningtech.news -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "index.py", 6 | "use": "@vercel/python" 7 | } 8 | ], 9 | "routes": [ 10 | { 11 | "src": "/(.*)", 12 | "dest": "/" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: psf/black@stable 11 | with: 12 | options: "--line-length 101" 13 | -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | upstream gmt { 2 | server web:5000; 3 | } 4 | 5 | server { 6 | 7 | listen 80; 8 | 9 | location / { 10 | proxy_pass http://gmt; 11 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 12 | proxy_set_header Host $host; 13 | proxy_redirect off; 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /gmt/static/JavaScript/landing-page/scroller.js: -------------------------------------------------------------------------------- 1 | const scroller = document.getElementById('scroller'); 2 | scroller.addEventListener('click', () => { 3 | window.scrollTo({ 4 | top: document.getElementById('what-do-we-offer').offsetTop, 5 | behavior: 'smooth' 6 | }); 7 | }); 8 | 9 | scroller.addEventListener('mouseover', () => { 10 | scroller.style.cursor = 'pointer'; 11 | }); -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v3.2.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - repo: https://github.com/psf/black 9 | rev: 22.12.0 10 | hooks: 11 | - id: black 12 | - repo: https://github.com/pycqa/isort 13 | rev: 5.11.4 14 | hooks: 15 | - id: isort 16 | -------------------------------------------------------------------------------- /gmt/static/loader.js: -------------------------------------------------------------------------------- 1 | const loader = document.querySelector('#loader-wrapper'); 2 | const isReduced = window.matchMedia(`(prefers-reduced-motion: reduce)`) === true || window.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true; 3 | 4 | if (!isReduced) { 5 | setTimeout(() => { 6 | loader.style.opacity = 0; 7 | loader.style.display = 'none'; 8 | }, 3000); 9 | } else { 10 | loader.style.display = 'none'; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | web: 3 | build: . 4 | container_name: goodmorningtech_web 5 | restart: unless-stopped 6 | environment: 7 | - FLASK_APP=index.py 8 | - FLASK_ENV=production 9 | volumes: 10 | - .:/app 11 | expose: 12 | - 5000 13 | 14 | nginx: 15 | build: ./nginx 16 | container_name: goodmorningtech_nginx 17 | restart: unless-stopped 18 | ports: 19 | - 5000:80 20 | depends_on: 21 | - web 22 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask-Mail==0.10.0 2 | Flask-PyMongo==2.3.0 3 | Flask-WTF==1.2.1 4 | Flask==2.3.2 5 | WTForms==3.2.1 6 | beautifulsoup4==4.12.3 7 | email-validator==2.1.1 8 | feedparser==6.0.11 9 | itsdangerous==2.2.0 10 | markdown==3.5 11 | requests==2.32.3 12 | Flask-Session2==1.3.1 13 | pymongo==4.8.0 14 | Flask-mde==1.2.1 15 | Flask-login==0.6.3 16 | arrow==1.3.0 17 | pytz==2022.7.1 18 | lxml==5.3.1 19 | Flask-Admin==1.6.1 20 | flask-crontab==0.1.2 21 | gunicorn==23.0.0 22 | flask-turnstile==0.1.1 23 | mistralai==1.5.1 -------------------------------------------------------------------------------- /gmt/templates/admin/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/master.html' %} 2 | {% block body %} 3 | {{ super() }} 4 |
5 | {% if current_user.is_authenticated and current_user.writer.email in config["ADMIN_USER_EMAILS"] %} 6 |

Logged in

7 | {% else %} 8 |

You don't have permissions to access the admin page.

9 | Go Home 10 | {% endif %} 11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /rss.json: -------------------------------------------------------------------------------- 1 | { 2 | "_comment": "This JSON file is for defining the URL and variables of different news sources in the feeds. TODO: add more sources and variables", 3 | "BBC": { 4 | "url": "https://feeds.bbci.co.uk/news/technology/rss.xml" 5 | }, 6 | "TechCrunch": { 7 | "url": "https://techcrunch.com/feed/" 8 | }, 9 | "Verge": { 10 | "url": "https://www.theverge.com/rss/index.xml" 11 | }, 12 | "Guardian": { 13 | "url": "https://www.theguardian.com/uk/technology/rss" 14 | }, 15 | "CNN": { 16 | "url": "http://rss.cnn.com/rss/cnn_tech.rss" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | target-branch: "development" 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Python image from the Docker Hub 2 | FROM python:3.11-bullseye 3 | 4 | # Set the working directory in the container 5 | WORKDIR /app 6 | 7 | # Install system dependencies 8 | RUN apt-get update && apt-get install -y \ 9 | cron \ 10 | && apt-get clean \ 11 | && rm -rf /var/lib/apt/lists/* 12 | 13 | # Copy the requirements file to the working directory 14 | COPY requirements.txt requirements.txt 15 | 16 | # Install the dependencies 17 | RUN pip install --no-cache-dir -r requirements.txt 18 | 19 | # Copy the rest of the application code to the working directory 20 | COPY . . 21 | 22 | RUN touch /var/log/cron.log && chmod 0644 /var/log/cron.log 23 | 24 | RUN python -m flask --app gmt crontab add 25 | 26 | RUN service cron start 27 | 28 | # Expose the port the app runs on 29 | EXPOSE 5000 30 | 31 | # Run the command on container startup 32 | CMD service cron start && gunicorn -b 0.0.0.0:5000 index:app -------------------------------------------------------------------------------- /gmt/static/JavaScript/writers/filter.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', () => { 2 | setFilter('newest') 3 | }) 4 | 5 | setFilter = (filter) => { 6 | let articles = document.querySelectorAll('#articles') 7 | let articlesArray = Array.from(articles[0].children) 8 | let articlesSorted = articlesArray.sort((a, b) => { 9 | return b.querySelector('.article-date').querySelector('.publish-date').textContent.localeCompare(a.querySelector('.article-date').querySelector('.publish-date').textContent) 10 | }) 11 | if (filter === 'oldest') { 12 | articlesSorted.reverse() 13 | } else if (filter === 'popularity') { 14 | articlesSorted.sort((a, b) => { 15 | return parseInt(b.querySelector('.views').textContent) - parseInt(a.querySelector('.views').textContent) 16 | }) 17 | } 18 | articlesSorted.forEach(article => { 19 | articles[0].appendChild(article) 20 | }) 21 | } -------------------------------------------------------------------------------- /gmt/templates/general/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | https://goodmorningtech.news/ 5 | 2023-01-25T22:13:30+01:00 6 | 1.0 7 | 8 | 9 | https://goodmorningtech.news/about 10 | 2023-01-25T22:13:30+01:00 11 | 1.0 12 | 13 | 14 | https://goodmorningtech.news/contact 15 | 2023-01-25T22:13:30+01:00 16 | 1.0 17 | 18 | 19 | https://goodmorningtech.news/contribute 20 | 2023-01-25T22:13:30+01:00 21 | 1.0 22 | 23 | 24 | https://goodmorningtech.news/subscribe 25 | 2023-01-25T22:13:30+01:00 26 | 1.0 27 | 28 | 29 | https://goodmorningtech.news/writers/apply 30 | 2023-01-25T22:13:30+01:00 31 | 0.8 32 | 33 | -------------------------------------------------------------------------------- /.github/workflows/black.yml: -------------------------------------------------------------------------------- 1 | name: black-action 2 | on: [push, pull_request] 3 | jobs: 4 | linter_name: 5 | name: runner / black 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - name: Check files using the black formatter 10 | uses: rickstaa/action-black@v1 11 | id: action_black 12 | with: 13 | black_args: "." 14 | - name: Create Pull Request 15 | if: steps.action_black.outputs.is_formatted == 'true' 16 | uses: peter-evans/create-pull-request@v3 17 | with: 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | title: "Format Python code with psf/black push" 20 | commit-message: ":art: Format Python code with psf/black" 21 | body: | 22 | There appear to be some python formatting errors in ${{ github.sha }}. This pull request 23 | uses the [psf/black](https://github.com/psf/black) formatter to fix these issues. 24 | base: ${{ github.head_ref }} # Creates pull request onto pull request or commit branch 25 | branch: actions/black 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 GoodMorningTech 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config.template.py: -------------------------------------------------------------------------------- 1 | SECRET_KEY = "dev" # Set this to a secure value in production 2 | DOMAIN_NAME = "127.0.0.1:5000" # Set this to your domain in production, don't append a trailing slash 3 | MONGO_URI = "mongodb://127.0.0.1:27017/users" 4 | MAIL_SERVER = "smtp.gmail.com" 5 | MAIL_PORT = 465 6 | MAIL_USE_TLS = False 7 | MAIL_USE_SSL = True 8 | MAIL_USERNAME = "username" 9 | MAIL_PASSWORD = "password" 10 | OPENAI_API_KEY = "sk-something" # main summarization API key 11 | FTP_HOST = "0.0.0.0" 12 | FTP_USER = "username" 13 | FTP_PASSWORD = "password" 14 | ADMIN_USER_EMAILS = ["email@email.com"] # Users who will have access to the admin panel 15 | API_NINJA_KEY = "" # API key for API Ninja, Get it from https://api-ninjas.com/ required for surprise function in email 16 | MISTRAL_API_KEY = ( 17 | "" # API key for Mistral AI, Get it from https://mistral.ai/ required for summaries 18 | ) 19 | FORM_WEBHOOK = None 20 | WRITER_WEBHOOK = None # Webhook where we will get notified on a new application 21 | CRON_JOB_WEBHOOK = None # Webhook where we will get notified when running cron jobs 22 | TURNSTILE_SITE_KEY = "" # cloudflare Turnstile site key 23 | TURNSTILE_SECRET_KEY = "" # cloudflare Turnstile secret key 24 | -------------------------------------------------------------------------------- /gmt/static/JavaScript/layout/navbar.js: -------------------------------------------------------------------------------- 1 | const button = document.querySelector('#hamburger-menu'); 2 | const menu = document.querySelector('#navigation-items'); 3 | 4 | button.addEventListener('click', () => { 5 | menu.classList.toggle('hidden'); 6 | }); 7 | 8 | const subscribe = document.querySelector('#subscribe'); 9 | const heroSection = document.querySelector('#hero-section'); 10 | 11 | document.addEventListener('DOMContentLoaded', () => { 12 | const navbarItem = document.getElementById(document.title); 13 | // if the page is active, add the red border to the navbar item 14 | if (navbarItem) { 15 | navbarItem.classList.add('md:border-t-gmt-red-primary', 'md:border-b-0'); 16 | navbarItem.classList.remove('md:border-t-gmt-bg', 'dark:md:border-t-gmt-dark-bg'); 17 | } 18 | else { 19 | const navigationItems = document.getElementById('navbar-item-list'); 20 | for (let i = 0; i < navigationItems.children.length; i++) { 21 | navigationItems.children[i].children[0].classList.add('md:border-t-gmt-red-primary', 'md:border-b-0'); 22 | navigationItems.children[i].children[0].classList.remove('md:border-t-gmt-bg', 'dark:md:border-t-gmt-dark-bg'); 23 | } 24 | } 25 | }) -------------------------------------------------------------------------------- /.github/workflows/cron.yaml: -------------------------------------------------------------------------------- 1 | name: Email Sender 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | DOMAIN_NAME: ${{ secrets.DOMAIN_NAME }} 7 | MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }} 8 | MAIL_PORT: ${{ secrets.MAIL_PORT }} 9 | MAIL_SERVER: ${{ secrets.MAIL_SERVER }} 10 | MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }} 11 | MAIL_USE_SSL: ${{ secrets.MAIL_USE_SSL }} 12 | MAIL_USE_TLS: ${{ secrets.MAIL_USE_TLS }} 13 | MONGO_DATABASE: ${{ secrets.MONGO_DATABASE }} 14 | MONGO_URI: ${{ secrets.MONGO_URI }} 15 | SECRET_KEY: ${{ secrets.SECRET_KEY }} 16 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 17 | API_NINJA_KEY: ${{ secrets.API_NINJA_KEY }} 18 | CRON_JOB_WEBHOOK: ${{ secrets.CRON_JOB_WEBHOOK }} 19 | 20 | jobs: 21 | cron: 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | 27 | - name: Setup Python 28 | uses: actions/setup-python@v4 29 | with: 30 | python-version: '3.10' 31 | cache: pip 32 | 33 | - name: Install dependencies 34 | run: | 35 | python -m pip install --upgrade pip 36 | pip install -r requirements.txt 37 | 38 | - name: Send the emails 39 | run: | 40 | python -m flask --app gmt commands send-emails 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "goodmorningtech", 3 | "version": "1.0.0", 4 | "description": "Get a daily dose of tech news in your mailbox! Good Morning Tech is a daily newsletter that delivers the most important tech news of the day. It's a great way to stay up to date with the latest tech news without having to spend hours on the internet.\r It's 100% automated and it's free! You can even set your time zone so that you get the news at the right time.", 5 | "main": "index.js", 6 | "scripts": { 7 | "tailwind": "npx tailwindcss -i ./gmt/static/config.css -o ./gmt/static/tailwind.css --watch", 8 | "tailwind:build": "npx tailwindcss -i ./gmt/static/config.css -o ./gmt/static/tailwind.css --minify" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/GoodMorninTech/GoodMorningTech.git" 13 | }, 14 | "keywords": [], 15 | "author": "GoodMorningTech", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/GoodMorninTech/GoodMorningTech/issues" 19 | }, 20 | "devDependencies": { 21 | "@tailwindcss/typography": "^0.5.9", 22 | "prettier": "^3.0.0", 23 | "tailwindcss": "^3.4.3" 24 | }, 25 | "homepage": "https://goodmorningtech.news/", 26 | "dependencies": { 27 | "flowbite": "^1.6.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for this project 3 | labels: enhancement 4 | title: "[Feature]: " 5 | body: 6 | - type: input 7 | attributes: 8 | label: Summary 9 | description: > 10 | A short summary of what your feature request is. 11 | validations: 12 | required: true 13 | - type: textarea 14 | attributes: 15 | label: Is your feature request related to a problem? 16 | description: > 17 | if yes, what becomes easier or possible when this feature is implemented? 18 | validations: 19 | required: true 20 | - type: textarea 21 | attributes: 22 | label: Describe the solution you'd like 23 | description: > 24 | A clear and concise description of what you want to happen. 25 | validations: 26 | required: true 27 | - type: textarea 28 | attributes: 29 | label: Describe alternatives you've considered 30 | description: > 31 | A clear and concise description of any alternative solutions or features you've considered. 32 | validations: 33 | required: false 34 | 35 | 36 | - type: textarea 37 | attributes: 38 | label: Additional Context 39 | description: Add any other context or screenshots about the feature request here. -------------------------------------------------------------------------------- /.github/workflows/articles.yaml: -------------------------------------------------------------------------------- 1 | name: Summarize News 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | DOMAIN_NAME: ${{ secrets.DOMAIN_NAME }} 7 | MAIL_DEFAULT_SENDER: ${{ secrets.MAIL_DEFAULT_SENDER }} 8 | MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }} 9 | MAIL_PORT: ${{ secrets.MAIL_PORT }} 10 | MAIL_SERVER: ${{ secrets.MAIL_SERVER }} 11 | MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }} 12 | MAIL_USE_SSL: ${{ secrets.MAIL_USE_SSL }} 13 | MAIL_USE_TLS: ${{ secrets.MAIL_USE_TLS }} 14 | MONGO_DATABASE: ${{ secrets.MONGO_DATABASE }} 15 | MONGO_URI: ${{ secrets.MONGO_URI }} 16 | SECRET_KEY: ${{ secrets.SECRET_KEY }} 17 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 18 | MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }} 19 | CRON_JOB_WEBHOOK: ${{ secrets.CRON_JOB_WEBHOOK }} 20 | 21 | jobs: 22 | cron: 23 | runs-on: ubuntu-latest 24 | 25 | steps: 26 | - uses: actions/checkout@v3 27 | 28 | - name: Setup Python 29 | uses: actions/setup-python@v4 30 | with: 31 | python-version: '3.10' 32 | cache: pip 33 | 34 | - name: Install dependencies 35 | run: | 36 | python -m pip install --upgrade pip 37 | pip install -r requirements.txt 38 | 39 | - name: Summarize Articles 40 | run: | 41 | python -m flask --app gmt commands summarize-news 42 | -------------------------------------------------------------------------------- /gmt/static/JavaScript/signup/signup.js: -------------------------------------------------------------------------------- 1 | const email = document.getElementById('email'); 2 | const sources = document.getElementById('source-list'); 3 | 4 | email.addEventListener("input", function (event) { 5 | if (email.validity.typeMismatch) { 6 | email.style.borderColor = "#F43434"; 7 | } else if (email.validity.valueMissing) { 8 | email.style.borderColor = "#ffb300"; 9 | } else { 10 | email.style.borderColor = "#15803d"; 11 | } 12 | }); 13 | 14 | document.addEventListener("DOMContentLoaded", () => { 15 | document.getElementById(Intl.DateTimeFormat().resolvedOptions().timeZone) 16 | .setAttribute("selected", "selected"); 17 | toggleButton(); 18 | }) 19 | 20 | sources.addEventListener("change", () => { 21 | toggleButton(); 22 | } 23 | ) 24 | 25 | const toggleButton = () => { 26 | let check_amount = 0; 27 | for (let i = 0; i < sources.children.length; i++) { 28 | if (sources.children[i].children[0].checked) { 29 | check_amount++; 30 | } 31 | } 32 | document.getElementById("subscribeButton").disabled = check_amount < 3; 33 | if (document.getElementById("subscribeButton").disabled) { 34 | document.getElementById("button-tooltip").style.display = "block"; 35 | } else { 36 | document.getElementById("button-tooltip").style.display = "none"; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /gmt/static/JavaScript/layout/darkmode.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', () => { 2 | if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) { 3 | document.documentElement.classList.add('dark') 4 | toggleVisuals() 5 | } else { 6 | document.documentElement.classList.remove('dark') 7 | toggleVisuals() 8 | } 9 | }) 10 | 11 | toggleDarkMode = () => { 12 | document.documentElement.classList.toggle('dark'); 13 | if (document.documentElement.classList.contains('dark')) { 14 | localStorage.theme = 'dark' 15 | } else { 16 | localStorage.theme = 'light' 17 | } 18 | toggleVisuals() 19 | } 20 | 21 | toggleVisuals = () => { 22 | let toggle = document.getElementById('dark-mode-toggle'); 23 | let dark = document.getElementById('dark-mode'); 24 | let light = document.getElementById('light-mode'); 25 | if (document.documentElement.classList.contains('dark')) { 26 | light.classList.remove('hidden'); 27 | dark.classList.add('hidden'); 28 | toggle.classList.remove('bg-gmt-pink') 29 | toggle.classList.add('bg-gmt-teal-secondary') 30 | } else { 31 | light.classList.add('hidden'); 32 | dark.classList.remove('hidden'); 33 | toggle.classList.remove('bg-gmt-teal-secondary') 34 | toggle.classList.add('bg-gmt-pink') 35 | } 36 | } -------------------------------------------------------------------------------- /gmt/templates/general/contribute.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Contribute{% endblock %} 3 | {% block body %} 4 |
5 |
6 | 7 | This page is currently under maintenance. 8 |
9 |
10 | You can still contribute to the project by visiting our GitHub Page & learn more about this project 12 | by joining our Discord Server. 14 |
15 |
16 | Additionally, if you would like to contribute to this project by becoming a writer, please visit our writer application page. 18 |
19 |
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | title: "[Bug]: " 3 | labels: bug 4 | description: Report broken or incorrect behaviour 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: > 9 | Thanks for taking the time to fill out a bug. 10 | Please note that this form is for bugs only! 11 | - type: textarea 12 | id: what-happened 13 | attributes: 14 | label: Describe the bug 15 | description: A clear and concise description of what the bug is. 16 | validations: 17 | required: true 18 | - type: textarea 19 | attributes: 20 | label: Reproduction Steps 21 | description: > 22 | What you did to make it happen. 23 | validations: 24 | required: true 25 | - type: textarea 26 | attributes: 27 | label: Expected behavior 28 | description: > 29 | A clear and concise description of what you expected to happen. 30 | validations: 31 | required: true 32 | - type: textarea 33 | attributes: 34 | label: Screenshots 35 | description: > 36 | If applicable, add screenshots to help explain your problem. 37 | validations: 38 | required: false 39 | - type: textarea 40 | attributes: 41 | label: System Information 42 | description: please fill your system informations 43 | value: > 44 | Operating System : [e.g. Windows 11] 45 | Browser : [e.g. Chrome 95] 46 | validations: 47 | required: true 48 | - type: textarea 49 | attributes: 50 | label: Additional Context 51 | description: Add any other context about the problem here. -------------------------------------------------------------------------------- /gmt/static/JavaScript/landing-page/typewriter.js: -------------------------------------------------------------------------------- 1 | /*I love codepen lol, credit for typewriter anim: https://codepen.io/Coding_Journey/pen/BEMgbX*/ 2 | const typedTextSpan = document.querySelector("#typed-text"); 3 | const cursorSpan = document.querySelector(".cursor"); 4 | 5 | const textArray = ["when you brew your coffee.", "when you start your day!", "while you commute to work.", "whenever you are not occupied."]; /*EDIT THIS ARRAY FOR MORE TEXT*/ 6 | const typingDelay = 175; 7 | const erasingDelay = 100; 8 | const newTextDelay = 2000; // Delay between current and next text 9 | let textArrayIndex = 0; 10 | let charIndex = 0; 11 | 12 | function type() { 13 | if (charIndex < textArray[textArrayIndex].length) { 14 | if(!cursorSpan.classList.contains("typing")) cursorSpan.classList.add("typing"); 15 | typedTextSpan.textContent += textArray[textArrayIndex].charAt(charIndex); 16 | charIndex++; 17 | setTimeout(type, typingDelay); 18 | } 19 | else { 20 | cursorSpan.classList.remove("typing"); 21 | setTimeout(erase, newTextDelay); 22 | } 23 | } 24 | 25 | function erase() { 26 | if (charIndex > 0) { 27 | if(!cursorSpan.classList.contains("typing")) cursorSpan.classList.add("typing"); 28 | typedTextSpan.textContent = textArray[textArrayIndex].substring(0, charIndex-1); 29 | charIndex--; 30 | setTimeout(erase, erasingDelay); 31 | } 32 | else { 33 | cursorSpan.classList.remove("typing"); 34 | textArrayIndex++; 35 | if(textArrayIndex>=textArray.length) textArrayIndex=0; 36 | setTimeout(type, typingDelay + 1100); 37 | } 38 | } 39 | 40 | document.addEventListener("DOMContentLoaded", function() { // On DOM Load initiate the effect 41 | if(textArray.length) setTimeout(type, newTextDelay + 250); 42 | }); -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./gmt/**/*.{html,js}", 5 | ], 6 | theme: { 7 | fontFamily: { 8 | 'gmt-fira': ['Fira Sans', 'sans-serif'], 9 | 'gmt-karla': ['Karla', 'sans-serif'], 10 | 'gmt-anonymous-pro': ['Anonymous Pro', 'monospace'], 11 | 'gmt-open-sans': ['Open Sans', 'sans-serif'], 12 | }, 13 | extend: { 14 | colors: { 15 | 'gmt-red-primary': '#CF3333', 16 | 'gmt-red-secondary': '#D02B2B', 17 | 'gmt-pink': '#FE7C84', 18 | 'gmt-black-primary': '#272727', 19 | 'gmt-yellow-primary': '#FFD037', 20 | 'gmt-teal-primary': '#1FC59D', 21 | 'gmt-teal-secondary': '#4A686C', 22 | 'gmt-gray-primary': '#646464', 23 | 'gmt-gray-secondary': '#CCCCCC', 24 | 'gmt-bg': '#F6F4F0', 25 | 'gmt-dark-bg': '#131313', 26 | 'gmt-bg-secondary': '#EBE9E4', 27 | 'gmt-dark-bg-secondary': '#333333', 28 | 'gmt-selected-navbar': '#E6E6E6', 29 | 'gmt-navbar-bg': '#F6F4F0', 30 | 'discord': '#5869E9', 31 | }, 32 | spacing: { 33 | 'gmt-112': '28rem', 34 | 'gmt-card': '40rem', 35 | 'gmt-128': '42rem', 36 | 'gmt-144': '56rem', 37 | }, 38 | screens: { 39 | '3xl': '1920px', 40 | }, 41 | backgroundImage: { 42 | 'contactPageBg': "url('https://cdn.goodmorningtech.news/website/contact/backgroundImageContactPage.png')", 43 | } 44 | }, 45 | }, 46 | darkMode: 'class', 47 | plugins: [ 48 | require('@tailwindcss/typography'), 49 | ], 50 | } -------------------------------------------------------------------------------- /gmt/static/config.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer utilities { 6 | .highlight { 7 | background: linear-gradient(to bottom, #EBE9E4, #EBE9E4 55%, rgba(221, 68, 68, 0.25) 55%, rgba(221, 68, 68, 0.25)); 8 | } 9 | 10 | .highlight-2 { 11 | background: linear-gradient(to bottom, white, white 55%, rgba(221, 68, 68, 0.25) 55%, rgba(221, 68, 68, 0.25)); 12 | } 13 | 14 | .highlight-3 { 15 | background: linear-gradient(to bottom, #f6f4f0, #f6f4f0 55%, rgba(221, 68, 68, 0.25) 55%, rgba(221, 68, 68, 0.25)); 16 | } 17 | 18 | .dark-highlight { 19 | background: linear-gradient(to bottom, #333333, #333333 55%, rgba(221, 68, 68, 0.25) 55%, rgba(221, 68, 68, 0.25)); 20 | } 21 | 22 | .dark-highlight-2 { 23 | background: linear-gradient(to bottom, black, black 55%, rgba(221, 68, 68, 0.25) 55%, rgba(221, 68, 68, 0.25)); 24 | } 25 | 26 | .dark-highlight-3 { 27 | background: linear-gradient(to bottom, #131313, #131313 55%, rgba(221, 68, 68, 0.25) 55%, rgba(221, 68, 68, 0.25)); 28 | } 29 | 30 | .cursor { 31 | display: inline-block; 32 | background-color: black; 33 | --blink-color: #272727; 34 | margin-left: 0.1rem; 35 | width: 3px; 36 | animation: blink 1s infinite; 37 | } 38 | 39 | .dark-cursor { 40 | display: inline-block; 41 | background-color: white; 42 | --blink-color: white; 43 | margin-left: 0.1rem; 44 | width: 3px; 45 | animation: blink 1s infinite; 46 | } 47 | 48 | 49 | @keyframes blink { 50 | 0% { 51 | background-color: var(--blink-color); 52 | } 53 | 49% { 54 | background-color: var(--blink-color); 55 | } 56 | 50% { 57 | background-color: transparent; 58 | } 59 | 99% { 60 | background-color: transparent; 61 | } 62 | 100% { 63 | background-color: var(--blink-color); 64 | } 65 | } 66 | } 67 | 68 | html { 69 | scroll-behavior: smooth; 70 | } 71 | -------------------------------------------------------------------------------- /gmt/news.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import re 4 | import feedparser 5 | import requests 6 | 7 | 8 | def get_posts(choice): 9 | """Get the posts from different RSS feeds.""" 10 | # Read the JSON file to get the variables and URL of the RSS feed, it looks like this 11 | # load the JSON file in the Flask app 12 | with open("rss.json") as f: 13 | rss = json.load(f) 14 | 15 | # Get the URL of the RSS feed 16 | url = rss[choice]["url"] 17 | # Get the feed 18 | feed = feedparser.parse(url) 19 | 20 | return feed.entries 21 | 22 | 23 | def convert_posts(posts, source, limit=8): 24 | """Convert the posts to a dict""" 25 | # Get the data from the posts 26 | data = [] 27 | for post in posts[:limit]: 28 | link = re.sub(r"[^\x00-\x7F]+", "", post.link) 29 | raw = requests.get(f"https://parser.goodmorningtech.news/parse?url={link}") 30 | try: 31 | if raw.status_code != 200: 32 | print( 33 | f"Error getting the data from {link}, status code: {raw.status_code}, request_link: {raw.url}" 34 | ) 35 | continue 36 | raw = raw.json() 37 | except json.decoder.JSONDecodeError: 38 | print("Error decoding JSON") 39 | continue 40 | 41 | image = raw["lead_image_url"] 42 | title = raw["title"] 43 | description = raw["content"] 44 | if not description: 45 | print(f"Error getting the description from {link}") 46 | continue 47 | date = raw["date_published"] 48 | author = raw["author"] 49 | print(f"Parsed Title: {title}") 50 | 51 | # Check if the post is from today UTC, the date is in YYYY-MM-DDTHH:MM:SS.000Z format 52 | from datetime import datetime 53 | from time import sleep 54 | 55 | if not date or date[:10] == datetime.utcnow().strftime("%Y-%m-%d"): 56 | data.append( 57 | { 58 | "title": title, 59 | "description": description, 60 | "url": post.link, 61 | "thumbnail": image, 62 | "author": author, 63 | "source": source, 64 | } 65 | ) 66 | else: 67 | continue 68 | sleep(1) 69 | 70 | return data 71 | 72 | 73 | def get_news(choice, limit=8): 74 | """Get the news""" 75 | # Get the posts 76 | posts = get_posts(choice) 77 | # Convert the posts to a dict 78 | data = convert_posts(posts, source=choice, limit=limit) 79 | return data 80 | -------------------------------------------------------------------------------- /gmt/templates/auth/success.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Success{% endblock %} 3 | {% block body %} 4 | 20 |
22 | {% if status == "subscribed" %} 23 |

24 | You have successfully subscribed to Good Morning Tech News! 25 | You will now receive your Tech news. 26 |

27 | {% elif status == "unsubscribed" %} 28 |

29 | You have successfully unsubscribed from Good Morning Tech 30 | News! 31 | You will no longer receive Tech news. 32 |

33 | {% elif status == "settings" %} 34 |

35 | You have successfully updated your settings! 36 |

37 | {% endif %} 38 | success 41 | Return Home 43 |

44 | Also check out: 45 | 47 | 48 | Morning the Bot 49 | 50 |

51 |
52 | {% endblock %} 53 | -------------------------------------------------------------------------------- /gmt/static/JavaScript/writers/create-articles.js: -------------------------------------------------------------------------------- 1 | const checkbox = document.getElementById('is_published'); 2 | const submitBtn = document.getElementById('submit-btn'); 3 | checkbox.addEventListener('change', (event) => { 4 | submitBtn.disabled = !event.target.checked; 5 | }); 6 | const preview = document.getElementById('wmd-preview'); 7 | const input = document.getElementById('wmd-input'); 8 | 9 | preview.addEventListener('DOMSubtreeModified', () => { 10 | if (preview.offsetHeight > 300) { 11 | input.style.height = preview.offsetHeight + 'px'; 12 | } 13 | }); 14 | 15 | input.addEventListener('input', () => { 16 | if (input.value === '') { 17 | input.style.height = '300px'; 18 | } 19 | }); 20 | 21 | // on load function 22 | document.addEventListener('DOMContentLoaded', () => { 23 | setTimeout(() => { 24 | // gets the ul element which contains li items with span children(the icons) 25 | const iconList = document.getElementById('wmd-button-row'); 26 | for (let i = 0; i < iconList.children.length; i++) { 27 | // checks if the item is a button 28 | if (iconList.children[i].classList.contains('wmd-button')) 29 | // sets the background image of the span child 30 | iconList.children[i].children[0].style.backgroundImage = "url('https://cdn.goodmorningtech.news/website/writers/iconpack.png')"; 31 | } 32 | }, 1000) 33 | }); 34 | 35 | toggleCategory = (category) => { 36 | const element = document.getElementById(category); 37 | const cross = element.parentElement.getElementsByClassName('fas ml-2')[0] 38 | if (cross.classList.contains('fa-plus')) { 39 | if (categories === maxCategories) { 40 | element.checked = false; 41 | alert('You can only add 3 categories to an article. To add more remove other ones.'); 42 | return; 43 | } else { 44 | categories++; 45 | } 46 | } else { 47 | categories--; 48 | } 49 | element.parentElement.classList.toggle('border-[1px]'); 50 | element.parentElement.classList.toggle('border-[2px]'); 51 | element.parentElement.classList.toggle('shadow-lg'); 52 | element.parentElement.classList.toggle('border-black'); 53 | 54 | // gets text-somecolor-800 class from parent and adds a border with that color 55 | const color = element.parentElement.classList.toString().split(' ').filter((item) => item.includes('text-')).filter((item) => item.includes('-800'))[0].split('text-')[1]; 56 | element.parentElement.classList.toggle(`border-${color}`); 57 | 58 | cross.classList.toggle('fa-plus'); 59 | cross.classList.toggle('fa-xmark'); 60 | } 61 | 62 | const categoryList = document.getElementById('category-list'); 63 | const maxCategories = 3; 64 | let categories = 0; 65 | -------------------------------------------------------------------------------- /gmt/views/api.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from bson import ObjectId 4 | from flask import Blueprint, Response, render_template, request, current_app 5 | from flask_login import current_user 6 | from flask_mail import Message 7 | 8 | from gmt import mongo, mail 9 | from gmt.utils import parse_json, rate_limit 10 | 11 | bp = Blueprint("api", __name__) 12 | 13 | 14 | @bp.route("/api/", methods=("POST", "GET")) 15 | def api(): 16 | if current_user.is_authenticated: 17 | current_user.writer = mongo.db.writers.find_one( 18 | {"_id": ObjectId(current_user.id)} 19 | ) 20 | 21 | if request.method == "POST": 22 | user_email = request.form.get("email") 23 | 24 | user = mongo.db.users.find_one({"email": user_email, "confirmed": True}) 25 | if not user: 26 | return render_template( 27 | "api/api.html", 28 | error="To get an API key, you must be subscribed with the email you enter.", 29 | ) 30 | 31 | msg = Message( 32 | "Your API Key", 33 | recipients=[user_email], 34 | sender=("Good Morning Tech", current_app.config["MAIL_USERNAME"]), 35 | body=f"""The API key for your account is: {user["_id"]}\nIf you didn't request this, you can safely ignore this email.""", 36 | ) 37 | mail.send(msg) 38 | return render_template( 39 | "api/api.html", 40 | error=None, 41 | success="Your API key has been sent to your email address.", 42 | ) 43 | 44 | return render_template("api/api.html", error=None) 45 | 46 | 47 | @bp.route("/api/news/") 48 | @rate_limit(limit=100, per=60) 49 | def news(): 50 | api_key = request.headers.get("X-API-KEY") 51 | if not api_key: 52 | return Response(status=401) 53 | 54 | if not ObjectId.is_valid(api_key): 55 | return Response(status=401) 56 | 57 | user = mongo.db.users.find_one({"_id": ObjectId(api_key)}) 58 | # if the user with that id isn't in the db, return 401 59 | if not user: 60 | return Response(status=401) 61 | 62 | sources = request.args.get("sources") 63 | if sources: 64 | sources = sources.split(",") 65 | posts = list( 66 | mongo.db.articles.find( 67 | { 68 | "source": {"$in": sources}, 69 | "date": { 70 | "$gte": datetime.datetime.utcnow() 71 | - datetime.timedelta(hours=25) 72 | }, 73 | } 74 | ) 75 | ) 76 | else: 77 | posts = list( 78 | mongo.db.articles.find( 79 | { 80 | "date": { 81 | "$gte": datetime.datetime.utcnow() 82 | - datetime.timedelta(hours=25) 83 | } 84 | } 85 | ) 86 | ) 87 | 88 | return parse_json(posts) 89 | -------------------------------------------------------------------------------- /gmt/templates/general/privacy_policy.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Privacy Policy{% endblock %} 3 | {% block body %} 4 |
5 |

Privacy Policy

6 |
7 |

8 | We take your privacy seriously. Please read this Privacy Policy to learn more about how we collect, use and protect your personal information. 9 |

10 |

1. Information Collection and Use

11 |

We may collect the following types of personal information:

12 | 16 |

We use this information to send you news.

17 |

18 | We will not sell, distribute or lease your personal information to third parties unless we have your permission or are required by law to do so. 19 |

20 |

2. Security

21 |

22 | We are committed to ensuring that your information is secure. In order to prevent unauthorized access or disclosure, we have put in place suitable physical, electronic and managerial procedures to safeguard and secure the information we collect online. 23 |

24 |

3. Cookies

25 |

26 | We use cookies to keep the website running, like saving your log in. A cookie is a small text file that a website saves on your computer or mobile device when you visit the site. You can control cookies through your browser settings. 27 |

28 |

4. Links to Other Websites

29 |

30 | Our website may contain links to other websites of interest. However, once you have used these links to leave our site, you should note that we do not have any control over that other website. Therefore, we cannot be responsible for the protection and privacy of any information which you provide while visiting such sites and such sites are not governed by this Privacy Policy. 31 |

32 |

5. Changes to This Privacy Policy

33 |

34 | We reserve the right to update or change this Privacy Policy at any time. If we make material changes to this Privacy Policy, we will notify you either through the email address you have provided us, or by placing a prominent notice on our website. 35 |

36 |

37 | If you have any questions or concerns about this Privacy Policy, please contact us at support@goodmorningtech.news. 38 |

39 |
40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '33 7 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | with: 74 | category: "/language:${{matrix.language}}" 75 | -------------------------------------------------------------------------------- /gmt/templates/auth/unsubscribe.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Unsubscribe{% endblock %} 3 | {% block body %} 4 | 20 |
22 | 27 |
28 |

29 | We hate to see you leave... 30 |

31 |

32 | Enter your email below & we'll send you a link and some instructions to help you unsubscribe. 33 |

34 | {% if error %}

{{ error }}

{% endif %} 35 |
36 | 37 |
38 | 43 |
44 | 48 |
49 |
50 |

51 | In case you ever change your mind, we'll always be there for you. Visit this link. 52 |

53 |
54 |
55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /gmt/templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 11 | 12 | 13 | 14 | 16 | 21 | 404 | This page does not exist 22 | 23 | 24 | 31 |
32 | 33 |
34 | 39 |
40 |

404

41 |

42 | Hmm... You are not meant to be here, go back home traveler! 43 |

44 |
45 |
46 |
47 | 48 | 49 | -------------------------------------------------------------------------------- /gmt/templates/general/tos.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Terms of Service{% endblock %} 3 | {% block body %} 4 |
5 |

Terms of Service

6 |
7 |

8 | By using our website, you agree to comply with and be bound by the following terms and conditions of use: 9 |

10 |

1. Introduction

11 |

12 | These Terms and Conditions ("Agreement") are between you ("User" or "you") and our company ("Company", "we", or "us"). This Agreement sets forth the general terms and conditions of your use of the website and any of its products or services. 13 |

14 |

2. Prohibited Uses

15 | 21 |

3. Disclaimer of Warranties

22 |

23 | The website and its products are provided “as is” without warranty of any kind, either express or implied, including without limitation warranties of merchantability, fitness for a particular purpose, or non-infringement. 24 |

25 |

4. Limitation of Liability

26 |

27 | In no event shall we or our suppliers be liable for any damages (including, without limitation, damages for loss of data or profit, or due to business interruption) arising out of the use or inability to use the materials on our website, even if we or an authorized representative has been notified orally or in writing of the possibility of such damage. 28 |

29 |

5. Indemnification

30 |

31 | You agree to indemnify and hold us harmless from any liability, loss, claim, and expense (including reasonable attorney's fees) related to your violation of this Agreement or use of the website and any of its products or services. 32 |

33 |

6. Governing Law

34 |

35 | This Agreement shall be governed by and construed in accordance with the laws of the State of California, United States, without giving effect to any principles of conflicts of law. 36 |

37 |

7. Changes to These Terms and Conditions

38 |

39 | We reserve the right, at our sole discretion, to modify or replace these Terms and Conditions at any time. If a revision is material, we will provide at least 30 days notice prior to any new terms taking effect. What constitutes a material change will be determined at our sole discretion. 40 |

41 |

If you have any questions about these Terms of Service, please contact us at support@goodmorningtech.news.

42 |
43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /gmt/templates/writers/login.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Login{% endblock %} 3 | {% block body %} 4 | 5 |
6 | 7 |
8 |

Writer Login

9 |

10 | Please enter your email and password to login, make sure you have applied & been accepted before logging in to your account. 11 |

12 | {% if status %} 13 | 18 | {% endif %} 19 |
20 | 21 | 29 |
30 | 37 |

38 | Forgot Password? 39 |

40 |
41 | 44 |
45 |

46 | If you haven't applied: Apply Here 47 |

48 |

49 | If you got accepted and don't have an account: Register Here. 50 |

51 |
52 |
53 | {% endblock %} 54 | -------------------------------------------------------------------------------- /gmt/templates/auth/confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Confirm Email{% endblock %} 3 | {% block head %}{% endblock %} 4 | {% block body %} 5 |
6 | 7 | {% if status == "sent" %} 8 |
9 | {#
#} 11 | {# #} 12 | {# Due to some technical issues with our email provider, the email will be sent from#} 13 | {# goodmorningtechnews@gmail.com.#} 14 | {#
#} 15 | {#
#} 16 | {% if error %} 17 |

18 | {{ error }} 19 |

20 | {% endif %} 21 |
22 |

Almost there...

23 |

24 | We have sent you a verification link at {{ email }}. 25 |
26 | Please check your inbox and click the confirm email link available to confirm your Email. 27 |

28 | 31 |

32 | Did not receive a link? Check your spam or click here to send again. 33 |

34 |
35 |
36 | {% elif status == "received" %} 37 |
38 |
39 | {% if error %} 40 |

41 | {{ error }} 42 |

43 | {% endif %} 44 |

45 | Confirm your email by 46 | clicking the button below. 47 |

48 |
49 | 53 |
54 |
55 |
56 | {% endif %} 57 |
58 | {% endblock %} 59 | -------------------------------------------------------------------------------- /gmt/templates/writers/guidelines.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Terms of Service{% endblock %} 3 | {% block body %} 4 |
5 |

Writer Guidelines

6 |
7 |

8 | These guidelines outline the expectations for writers creating content on behalf of Good Morning Tech for use in our newsletters. These guidelines apply to any writer who creates content on behalf of the Company, including employees and independent contractors. 9 |

10 |

Original content

11 |

12 | Good Morning Tech values original research and insights. All writers must strive to deliver unique information, whether through exclusive data or interviews with industry experts. Plagiarism is strictly prohibited. 13 |

14 |

Tone

15 |

16 | Our newsletters are personal in nature. Therefore, writers should maintain a conversational tone that is engaging and easy to read. Technical language and jargon should be avoided whenever possible. All content should be written in a manner that is sensitive to our readers. 17 |

18 |

Supporting Evidence

19 |

20 | While Good Morning Tech values unique insights, it is also critical to provide supporting evidence for such insights. This includes links to relevant studies, reports or news articles. Writers should always respect copyright laws and fair use guidelines when referencing other sources. 21 |

22 |

Objectivity and Bias

23 |

24 | Our readers trust us to provide accurate and balanced information. Therefore, writers must remain objective and unbiased in their writing. If an article has a strong point of view, it must be clearly labeled as an opinion piece. The Company reserves the right to reject content which breaches its standards of impartiality. 25 |

26 |

Attribution

27 |

28 | If a writer references another source in their work, it is essential that they provide clear attribution. This includes proper citation and providing a link to the original article wherever possible. The Company retains the right to verify all references made in any submitted content. 29 |

30 |

Proofreading and Fact-Checking

31 |

32 | Accuracy is paramount when it comes to writing for our newsletters. Therefore, all writers must thoroughly proofread their work and fact-check any information before submitting it. The Company reserves the right to verify all facts presented in any submitted content. 33 |

34 |

Feedback

35 |

36 | Writers must be open to receiving feedback from editors and other team members. This will help to ensure that the final product is the best it can be. All critique and feedback will be delivered in a respectful and constructive manner. 37 |

38 |

Conclusion

39 |

40 | Good Morning Tech values originality, accuracy, objectivity, and professionalism in all content created for our newsletters. By adhering to these guidelines, writers will be contributing towards maintaining standards and quality of our newsletters. 41 |

42 |

Acceptance

43 |

44 | All writers must accept these guidelines before creating their first article. By submitting content or accepting payment, writers acknowledge their acceptance of these guidelines. 45 |

46 |
47 | {% endblock %} 48 | -------------------------------------------------------------------------------- /gmt/utils.py: -------------------------------------------------------------------------------- 1 | import html 2 | import json 3 | import random 4 | from ftplib import FTP 5 | from bson import json_util 6 | import time 7 | from flask import request 8 | from collections import deque 9 | 10 | # API rate limiting 11 | call_history = {} 12 | 13 | 14 | def rate_limit(limit=100, per=60): 15 | def decorator(handler): 16 | def wrapper(*args, **kwargs): 17 | ip_address = request.remote_addr 18 | 19 | if ip_address not in call_history: 20 | call_history[ip_address] = deque() 21 | 22 | queue = call_history[ip_address] 23 | current_time = time.time() 24 | 25 | while len(queue) > 0 and current_time - queue[0] > per: 26 | queue.popleft() 27 | 28 | if len(queue) >= limit: 29 | time_passed = current_time - queue[0] 30 | time_to_wait = int(per - time_passed) 31 | error_message = ( 32 | f"Rate limit exceeded. Please try again in {time_to_wait} seconds." 33 | ) 34 | return error_message, 429 35 | 36 | queue.append(current_time) 37 | 38 | return handler(*args, **kwargs) 39 | 40 | return wrapper 41 | 42 | return decorator 43 | 44 | 45 | def clean_html(html_string): 46 | return ( 47 | html.escape(html_string, quote=False) 48 | .replace("", "</script>") 50 | .replace("", "</style>") 52 | ) 53 | 54 | 55 | def parse_json(data): 56 | return json.loads(json_util.dumps(data)) 57 | 58 | 59 | allowed_file_types = lambda filename: "." in filename and filename.rsplit(".", 1)[ 60 | 1 61 | ].lower() in ["png", "jpg", "jpeg"] 62 | 63 | 64 | def upload_file(file, filename, current_app): 65 | # TODO Convert the images to one format, so we can use the same extension in /portal 66 | 67 | if file.filename and allowed_file_types(file.filename): 68 | # Rename the file to the user_name 69 | file.filename = f"{filename}.jpg" 70 | # Connect to FTP server 71 | ftp = FTP(current_app.config["FTP_HOST"]) 72 | ftp.login( 73 | user=current_app.config["FTP_USER"], 74 | passwd=current_app.config["FTP_PASSWORD"], 75 | ) 76 | # Upload file to the directory htdocs/images 77 | if file.filename in ftp.nlst("htdocs"): 78 | ftp.delete(f"htdocs/{file.filename}") 79 | ftp.storbinary(f"STOR /htdocs/{file.filename}", file) 80 | # Close FTP connection 81 | ftp.quit() 82 | return True 83 | elif file.filename and not allowed_file_types(file.filename): 84 | return False 85 | 86 | 87 | def format_html(text): 88 | # Replace '\n' with '
' 89 | text = text.replace("\n", "
") 90 | text = text.replace("\t", "") 91 | # Add styling to tag 92 | text = text.replace( 93 | "", 94 | '', 95 | ) 96 | text = text.replace("", "") 97 | return text 98 | 99 | 100 | def random_language_greeting(): 101 | json = { 102 | "english": "Good Morning", 103 | "spanish": "¡Buenos días", 104 | "chinese": "早上好!", 105 | "hindi": "शुभ प्रभात", 106 | "arabic": "صباح الخير", 107 | "portuguese": "Bom dia", 108 | "bengali": "শুভ সকাল", 109 | "russian": "Доброе утро", 110 | "japanese": "おはようございます!", 111 | "punjabi": "ਸ਼ੁਭ ਸਵੇਰ", 112 | "german": "Guten Morgen", 113 | "georgian": "დილა მშვიდობის", 114 | "korean": "안녕하세요", 115 | "french": "Bonjour", 116 | "turkish": "Günaydın", 117 | "italian": "Buongiorno", 118 | "urdu": "صبح بخیر", 119 | "polish": "Dzień dobry", 120 | "javanese": "Selamat pagi", 121 | "marathi": "शुभ प्रभात", 122 | "dutch": "Goedemorgen", 123 | } 124 | language, value = random.choice(list(json.items())) 125 | return language.capitalize(), value 126 | -------------------------------------------------------------------------------- /gmt/views/articles.py: -------------------------------------------------------------------------------- 1 | from bson import ObjectId 2 | import markdown 3 | from flask import ( 4 | Blueprint, 5 | abort, 6 | redirect, 7 | render_template, 8 | request, 9 | session, 10 | url_for, 11 | current_app, 12 | ) 13 | from datetime import datetime 14 | 15 | from flask_login import login_required, current_user 16 | 17 | from .. import mongo 18 | from ..utils import clean_html, upload_file 19 | 20 | bp = Blueprint("articles", __name__, url_prefix="/articles") 21 | 22 | 23 | @bp.route("/", methods=("POST", "GET")) 24 | def article(article_id): 25 | if current_user.is_authenticated: 26 | current_user.writer = mongo.db.writers.find_one( 27 | {"_id": ObjectId(current_user.id)} 28 | ) 29 | 30 | article_db = mongo.db.articles.find_one({"_id": ObjectId(article_id)}) 31 | # if article doesnt exists 404 32 | if not article_db: 33 | return render_template("404.html") 34 | 35 | if request.method == "POST": 36 | # DELETES ARTICLE 37 | if current_user.is_authenticated: 38 | mongo.db.articles.delete_one({"_id": ObjectId(article_id)}) 39 | return redirect(url_for("writers.portal")) 40 | 41 | content_md = markdown.markdown(article_db["content"]) 42 | 43 | date = article_db["date"].strftime("%d %B %Y") 44 | 45 | article_db["views"] = int(article_db["views"]) + 1 46 | mongo.db.articles.update_one( 47 | {"_id": ObjectId(article_id)}, {"$set": {"views": article_db["views"]}} 48 | ) 49 | 50 | return render_template( 51 | "articles/article.html", 52 | article=article_db, 53 | content=content_md, 54 | date=date, 55 | no_meta=True, 56 | ) 57 | 58 | 59 | @bp.route("/edit/", methods=("POST", "GET")) 60 | @login_required 61 | def edit(article_id): 62 | current_user.writer = mongo.db.writers.find_one({"_id": ObjectId(current_user.id)}) 63 | 64 | article_db = mongo.db.articles.find_one({"_id": ObjectId(article_id)}) 65 | if not article_db: 66 | return render_template("404.html") 67 | if article_db["author"]["email"] != current_user.writer["email"]: 68 | return abort(403) 69 | 70 | if request.method == "POST": 71 | title = request.form.get("title") 72 | if not title: 73 | return render_template( 74 | "writers/create.html", 75 | status=f"Please enter a title!", 76 | article=article_db, 77 | ) 78 | description = request.form.get("description") 79 | if not description: 80 | return render_template( 81 | "writers/create.html", 82 | status=f"Please enter a description!", 83 | article=article_db, 84 | ) 85 | content = request.form.get("content") 86 | if not content: 87 | return render_template( 88 | "writers/create.html", 89 | status=f"Please enter some content!", 90 | article=article_db, 91 | ) 92 | thumbnail = request.files.get("thumbnail", None) 93 | categories = request.form.getlist("category") 94 | 95 | if not categories: 96 | return render_template( 97 | "writers/create.html", 98 | status=f"Please select atleast one category!", 99 | article=article_db, 100 | ) 101 | 102 | if thumbnail: 103 | if not upload_file( 104 | file=thumbnail, filename=article_db["_id"], current_app=current_app 105 | ): 106 | return render_template( 107 | "writers/create.html", 108 | status=f"Error uploading thumbnail! Uploaded without thumbnail," 109 | f" edit article to add one!", 110 | article=article_db, 111 | ) 112 | 113 | mongo.db.articles.update_one( 114 | {"_id": ObjectId(article_id)}, 115 | { 116 | "$set": { 117 | "title": title, 118 | "description": description, 119 | "content": clean_html(content), 120 | "categories": categories, 121 | } 122 | }, 123 | ) 124 | return redirect(url_for("articles.article", article_id=article_id)) 125 | 126 | return render_template("articles/edit.html", article=article_db) 127 | -------------------------------------------------------------------------------- /gmt/templates/general/credits.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Credits and Tech{% endblock %} 3 | {% block body %} 4 |
5 |

How it's made: Good Morning Tech

6 |
7 | Designed with 8 |
9 |

Figma, Adobe Photoshop and Dribbble

10 |
11 | Built with 12 |
13 |

Python for the backend. HTML5, CSS3 and Javascript for the frontend

14 |
15 | Tech Stack 16 |
17 |

18 | For the backend we used Flask together with MongoDB for the Database. For the 19 | frontend we used Jinja Templates and Tailwind. For the website hosting we used Vercel and for the emails and 20 | Images we used Cybrancee. 21 |
22 | For a more detailed technical breakdown, check out Levani's article about it. (not yet written) 23 |

24 |
25 | Developed by 26 |
27 |

28 | Good Morning Tech is developed by: 29 |
30 | @HarryDaDev(Design 31 | and Frontend) 32 |
33 | @OpenSourceSimon(Backend) 34 |
35 | @LevaniVashadze(Backend 36 | and Frontend) 37 |
38 | @Kappa(Backend) 39 |
40 | More Contributors are on the GitHub page, and we are always looking for more contributors. 41 |

42 |
43 | Additionally 44 |
45 |

46 | Special thanks to Lewis Menelaws(Coding with Lewis) for having faith in the project and giving us 47 | the goodmorningtech.news domain. 48 |

49 |
50 | Disclaimer news content 51 |
52 |

53 | We would like to take a moment to clarify that not all of the news content on 54 | our website is our original work. As a news aggregator, we strive to provide our readers with the latest and 55 | most accurate news from various reliable sources. 56 | While we do publish some original content, a significant portion of the news articles you will find on our 57 | website are sourced from other news outlets. We carefully select and curate the news stories that we believe are 58 | most relevant and informative to our readers, and we always provide proper attribution to the original sources. 59 | We encourage our readers to visit the original sources of the news articles we publish to gain a more 60 | comprehensive understanding of the topics covered. We also welcome any feedback or suggestions on how we can 61 | improve our news coverage and better serve our readers. 62 | Thank you for your continued support and trust in our website as a source of news and information. 63 |

64 |
65 | {% endblock %} 66 | -------------------------------------------------------------------------------- /gmt/static/JavaScript/writers/user-apply.js: -------------------------------------------------------------------------------- 1 | let totalSteps = document.getElementById('totalSteps'); 2 | let currentStep = document.getElementById('currentStep'); 3 | 4 | let email = document.getElementById('userEmail'); 5 | let contact = document.getElementById('userContact'); 6 | let displayName = document.getElementById('username'); 7 | 8 | let stepOne = document.getElementById('stepOne'); 9 | let stepOneBtn = document.getElementById('nextBtnStepOne'); 10 | 11 | const progressBar = document.getElementById('progressBar'); 12 | 13 | stepOneBtn.addEventListener('click', function () { 14 | if (email.value === '' || contact.value === '' || displayName.value === '') { 15 | if (email.value === '') { 16 | email.classList.add('border-red-500', 'border-2'); 17 | } 18 | if (contact.value === '') { 19 | contact.classList.add('border-red-500', 'border-2'); 20 | } 21 | if (displayName.value === '') { 22 | displayName.classList.add('border-red-500', 'border-2'); 23 | } 24 | } else { 25 | stepOne.classList.add('hidden'); 26 | stepTwo.classList.remove('hidden'); 27 | currentStep.textContent = 2; 28 | progressBar.classList.remove('w-1/3'); 29 | progressBar.classList.add('w-2/3'); 30 | } 31 | }); 32 | 33 | email.addEventListener('blur', function () { 34 | email.classList.remove('border-red-500', 'border-2'); 35 | }); 36 | contact.addEventListener('blur', function () { 37 | contact.classList.remove('border-red-500', 'border-2'); 38 | }); 39 | displayName.addEventListener('blur', function () { 40 | displayName.classList.remove('border-red-500', 'border-2'); 41 | }); 42 | 43 | const checkEmail = () => { 44 | if (email.value.includes('@') && email.value.includes('.')) { 45 | email.classList.remove('border-red-500', 'border-2'); 46 | email.classList.add('border-green-500', 'border-2'); 47 | } else { 48 | email.classList.remove('border-green-500', 'border-2'); 49 | email.classList.add('border-red-500', 'border-2'); 50 | } 51 | } 52 | email.addEventListener('click', checkEmail); 53 | email.addEventListener('input', checkEmail); 54 | email.addEventListener('blur', checkEmail); 55 | 56 | let goBackBtnStepTwo = document.getElementById('backBtnStepTwo'); 57 | let stepTwoBtn = document.getElementById('nextBtnStepTwo'); 58 | let stepTwo = document.getElementById('stepTwo'); 59 | 60 | let whyWrite = document.getElementById('whyWrite'); 61 | let topics = document.getElementById('topics'); 62 | 63 | goBackBtnStepTwo.addEventListener('click', function () { 64 | stepTwo.classList.add('hidden'); 65 | stepOne.classList.remove('hidden'); 66 | currentStep.textContent = 1; 67 | progressBar.classList.remove('w-2/3'); 68 | progressBar.classList.add('w-1/3'); 69 | }); 70 | 71 | stepTwoBtn.addEventListener('click', function () { 72 | if (whyWrite.value === '' || topics.value === '') { 73 | if (whyWrite.value === '') { 74 | whyWrite.classList.add('border-red-500', 'border-2'); 75 | } 76 | if (topics.value === '') { 77 | topics.classList.add('border-red-500', 'border-2'); 78 | } 79 | } else { 80 | stepTwo.classList.add('hidden'); 81 | stepThree.classList.remove('hidden'); 82 | currentStep.textContent = 3; 83 | progressBar.classList.remove('w-2/3'); 84 | progressBar.classList.add('w-full'); 85 | } 86 | } 87 | ); 88 | 89 | whyWrite.addEventListener('blur', function () { 90 | whyWrite.classList.remove('border-red-500', 'border-2'); 91 | }); 92 | 93 | topics.addEventListener('blur', function () { 94 | topics.classList.remove('border-red-500', 'border-2'); 95 | }); 96 | 97 | let goBackBtnStepThree = document.getElementById('backBtnStepThree'); 98 | let stepThree = document.getElementById('stepThree'); 99 | 100 | goBackBtnStepThree.addEventListener('click', function () { 101 | stepThree.classList.add('hidden'); 102 | stepTwo.classList.remove('hidden'); 103 | currentStep.textContent = 2; 104 | progressBar.classList.remove('w-full'); 105 | progressBar.classList.add('w-2/3'); 106 | }); 107 | 108 | let submitFormBtn = document.getElementById('submitFormBtn'); 109 | let sample = document.getElementById('sample'); 110 | let article = document.getElementById('article'); 111 | let checkbox = document.getElementById('terms'); 112 | 113 | const checkButton = () => { 114 | submitFormBtn.disabled = article.value === '' || checkbox.checked === false; 115 | } 116 | 117 | document.addEventListener('DOMContentLoaded', checkButton); 118 | article.addEventListener('input', checkButton); 119 | checkbox.addEventListener('click', checkButton); 120 | 121 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to GoodMorningTech 💻📝 2 | 3 | Thank you for your interest in contributing to GoodMorningTech! 🙌 Whether you're a programmer 💻, writer 📝, or community helper 🤝, your contributions are greatly appreciated. 4 | 5 | ## Table of Contents 6 | 1. [How to Contribute](#how-to-contribute-) 7 | 2. [Code of Conduct](#code-of-conduct-) 8 | 3. [Getting Started](#getting-started-) 9 | 4. [Bug Reporting](#bug-reporting-) 10 | 5. [Enhancement Requests](#enhancement-requests-) 11 | 6. [Contact Information](#contact-information-) 12 | 13 | ## How to Contribute 🤔 14 | 15 | Here are a few ways you can contribute to the GoodMorningTech project: 16 | 17 | - **Programming**: If you have programming experience and would like to contribute to the code base, check out the [open issues](https://github.com/GoodMorninTech/GoodMorningTech/issues) to see what needs to be done or create an issue or pull request, and we will review it. Before you start working on a new feature or bug fix, make sure to fork the project and work on the development branch and open a pull request when you're ready to submit your changes. 💻 18 | 19 | - **Writing**: If you have a passion for writing and would like to contribute to our news, Apply at this link [Application Link](https://goodmorningtech.news/writers/apply), if we accept you we will reach out. 📝 20 | 21 | - **Community Help**: If you'd like to help out the community in any way, let us know, or join our [discord](https://discord.goodmorningtech.news)! We're always looking for ways to improve and make a positive impact. 🤝 22 | 23 | ## Code of Conduct 📜 24 | 25 | To ensure that the GoodMorningTech community is welcoming and inclusive to all, we have a [Code of Conduct](CODE_OF_CONDUCT.md) that all contributors must follow. Please take a moment to read it before contributing. 26 | 27 | ## Getting Started 🚀 28 | 29 | To get started fork the Repository on GitHub. 30 | 31 | ### Prerequisites 32 | - Python 33 | - Node.js 34 | 35 | If you don't have these just install them from the official websites. 36 | 37 | #### Cloning the repository 38 | Clone the forked repository(make sure to insert your username): 39 | ``` 40 | git clone https://github.com/YOURGITHUBUSERNAME/GoodMorningTech.git 41 | ``` 42 | Move into the new directory: 43 | ``` 44 | cd GoodMorningTech 45 | ``` 46 | #### Configuration 47 | Create an `instance` folder: 48 | ``` 49 | mkdir instance 50 | ``` 51 | Move the configuration template into `instance` and rename it to `config.py`: 52 | Windows: 53 | ``` 54 | copy config.template.py config.py 55 | move config.py instance 56 | ``` 57 | Linux: 58 | ``` 59 | cp config.template.py config.py 60 | mkdir instance 61 | mv config.py instance/config.py 62 | ``` 63 | Edit the configuration file and set the fields to your liking. 64 | 65 | For the database to work create a mongoDB database and set the `MONGO_URI` to your database. 66 | 67 | To use the email functionality set the `MAIL_USERNAME`,`MAIL_PASSWORD` and `MAIL_SERVER` 68 | to your email credentials, for gmail you might need to configure extra stuff, 69 | look up recent guides on how to use SMTP with gmail. 70 | 71 | Alternatively you can configure everything from environment variables, make sure to set all the variables in `config.template.py`. 72 | 73 | #### Set Up for Development 74 | Install the development requirements: 75 | ``` 76 | pip install -r requirements-dev.txt 77 | ``` 78 | ``` 79 | npm install 80 | ``` 81 | 82 | [//]: # (#### Install pre-commit hooks:) 83 | 84 | [//]: # (```) 85 | 86 | [//]: # (pre-commit install) 87 | 88 | [//]: # (```) 89 | 90 | #### Running the Server 91 | Install the requirements: 92 | ``` 93 | pip install -r requirements.txt 94 | ``` 95 | Run the application: 96 | ``` 97 | python -m flask --app gmt --debug run 98 | ``` 99 | and in separate terminal run the tailwind compiler: 100 | ``` 101 | npm run tailwind 102 | ``` 103 | 104 | ## Bug Reporting 🐛 105 | 106 | If you find a bug in the project, we would love to know about it! Before reporting a bug, please check the [open issues](https://github.com/GoodMorninTech/GoodMorningTech/issues) to see if it has already been reported. If not, please create a new issue and provide as much detail as possible about the bug, including steps to reproduce it. 107 | 108 | ## Enhancement Requests 💡 109 | 110 | If you have an idea for a new feature or enhancement, we'd love to hear about it! Before creating a new enhancement request, please check the [open issues](https://github.com/GoodMorninTech/GoodMorningTech/issues) to see if it has already been suggested. If not, please create a new issue and provide as much detail as possible about the enhancement. 111 | 112 | ## Contact Information 📞 113 | 114 | If you have any questions or would like to get in touch with us, feel free to send us an email at [support@goodmorningtech.news](mailto:support@goodmorningtech.news) or join our Discord server at [discord.goodmorningtech.news](https://discord.goodmorningtech.news/). We're always here to help and support you in your contributions! 🤗 115 | -------------------------------------------------------------------------------- /gmt/static/JavaScript/writers/create-profile.js: -------------------------------------------------------------------------------- 1 | let totalSteps = document.getElementById('totalSteps'); 2 | let currentStep = document.getElementById('currentStep'); 3 | 4 | let email = document.getElementById('userEmail'); 5 | let name = document.getElementById('username'); 6 | let displayName = document.getElementById('displayName'); 7 | 8 | let stepOne = document.getElementById('stepOne'); 9 | let stepOneBtn = document.getElementById('nextBtnStepOne'); 10 | 11 | const progressBar = document.getElementById('progressBar'); 12 | 13 | stepOneBtn.addEventListener('click', function () { 14 | if (email.value === '' || name.value === '' || displayName.value === '') { 15 | if (email.value === '') { 16 | email.classList.add('border-red-500', 'border-2'); 17 | } 18 | if (name.value === '') { 19 | name.classList.add('border-red-500', 'border-2'); 20 | } 21 | if (displayName.value === '') { 22 | displayName.classList.add('border-red-500', 'border-2'); 23 | } 24 | } else { 25 | stepOne.classList.add('hidden'); 26 | stepTwo.classList.remove('hidden'); 27 | currentStep.textContent = 2; 28 | progressBar.classList.remove('w-1/3'); 29 | progressBar.classList.add('w-2/3'); 30 | } 31 | }); 32 | 33 | email.addEventListener('blur', function () { 34 | email.classList.remove('border-red-500', 'border-2'); 35 | }); 36 | name.addEventListener('blur', function () { 37 | name.classList.remove('border-red-500', 'border-2'); 38 | }); 39 | displayName.addEventListener('blur', function () { 40 | displayName.classList.remove('border-red-500', 'border-2'); 41 | }); 42 | 43 | const checkEmail = () => { 44 | if (email.value.includes('@') && email.value.includes('.')) { 45 | email.classList.remove('border-red-500', 'border-2'); 46 | email.classList.add('border-green-500', 'border-2'); 47 | } else { 48 | email.classList.remove('border-green-500', 'border-2'); 49 | email.classList.add('border-red-500', 'border-2'); 50 | } 51 | } 52 | email.addEventListener('click', checkEmail); 53 | email.addEventListener('input', checkEmail); 54 | email.addEventListener('blur', checkEmail); 55 | 56 | let goBackBtnStepTwo = document.getElementById('backBtnStepTwo'); 57 | let stepTwoBtn = document.getElementById('nextBtnStepTwo'); 58 | let stepTwo = document.getElementById('stepTwo'); 59 | 60 | let password = document.getElementById('password'); 61 | let repeatPassword = document.getElementById('repeatPassword'); 62 | 63 | goBackBtnStepTwo.addEventListener('click', function () { 64 | stepTwo.classList.add('hidden'); 65 | stepOne.classList.remove('hidden'); 66 | currentStep.textContent = 1; 67 | progressBar.classList.remove('w-2/3'); 68 | progressBar.classList.add('w-1/3'); 69 | }); 70 | 71 | stepTwoBtn.addEventListener('click', function () { 72 | if (password.value === '' || repeatPassword.value === '') { 73 | if (password.value === '') { 74 | password.classList.add('border-red-500', 'border-2'); 75 | } 76 | if (repeatPassword.value === '') { 77 | repeatPassword.classList.add('border-red-500', 'border-2'); 78 | } 79 | } else { 80 | if (password.value === repeatPassword.value) { 81 | stepTwo.classList.add('hidden'); 82 | stepThree.classList.remove('hidden'); 83 | currentStep.textContent = 3; 84 | progressBar.classList.remove('w-2/3'); 85 | progressBar.classList.add('w-full'); 86 | } else { 87 | password.classList.add('border-red-500', 'border-2'); 88 | repeatPassword.classList.add('border-red-500', 'border-2'); 89 | window.alert('Your passwords do not match!'); 90 | } 91 | } 92 | }); 93 | 94 | password.addEventListener('blur', function () { 95 | password.classList.remove('border-red-500', 'border-2'); 96 | }); 97 | 98 | repeatPassword.addEventListener('blur', function () { 99 | repeatPassword.classList.remove('border-red-500', 'border-2'); 100 | }); 101 | 102 | let goBackBtnStepThree = document.getElementById('backBtnStepThree'); 103 | let stepThree = document.getElementById('stepThree'); 104 | 105 | goBackBtnStepThree.addEventListener('click', function () { 106 | stepThree.classList.add('hidden'); 107 | stepTwo.classList.remove('hidden'); 108 | currentStep.textContent = 2; 109 | progressBar.classList.remove('w-full'); 110 | progressBar.classList.add('w-2/3'); 111 | }); 112 | 113 | let submitFormBtn = document.getElementById('submitFormBtn'); 114 | let aboutMe = document.getElementById('aboutMe'); 115 | let timezone = document.getElementById('timezone'); 116 | let checkbox = document.getElementById('terms'); 117 | 118 | const checkButton = () => { 119 | submitFormBtn.disabled = aboutMe.value === '' || checkbox.checked === false; 120 | } 121 | 122 | document.addEventListener('DOMContentLoaded', checkButton); 123 | aboutMe.addEventListener('input', checkButton); 124 | checkbox.addEventListener('click', checkButton); 125 | 126 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement. 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series 85 | of actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or 92 | permanent ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within 112 | the community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.0, available at 118 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 119 | 120 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 121 | enforcement ladder](https://github.com/mozilla/diversity). 122 | 123 | [homepage]: https://www.contributor-covenant.org 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | https://www.contributor-covenant.org/faq. Translations are available at 127 | https://www.contributor-covenant.org/translations. 128 | -------------------------------------------------------------------------------- /gmt/templates/articles/article.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}{{ article.title }}{% endblock %} 3 | {% block head %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% endblock %} 16 | {% block body %} 17 |
18 |
19 |

{{ article.title }}

20 |

21 | Published on: {{ article.date.strftime("%B %d, %Y") }} 22 |

23 |

24 | Written by: 25 | {{ article.author.name }} 27 | 28 |

29 | 31 |
32 | {{ content|safe }} 33 |
34 | {% if current_user.is_authenticated and current_user.writer.email == article.author.email %} 35 |
36 | Edit this article 38 |
39 |
40 | 41 | 45 |
46 | 52 |
53 | 58 | {% endif %} 59 |
60 |
61 | {% endblock %} 62 | -------------------------------------------------------------------------------- /gmt/views/admin.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import pymongo 4 | import pytz 5 | from bson.objectid import ObjectId 6 | 7 | from flask import Flask, Blueprint, current_app 8 | from flask_admin import BaseView, expose 9 | from flask_login import current_user 10 | 11 | from .. import admin 12 | from .. import mongo 13 | 14 | from wtforms import form, fields 15 | 16 | from flask_admin.form import Select2Widget 17 | from flask_admin.contrib.pymongo import ModelView, filters 18 | from flask_admin.model.fields import InlineFormField, InlineFieldList 19 | 20 | # Create application 21 | bp = Blueprint("admins", __name__, url_prefix="/admin") 22 | 23 | 24 | class SecureModelView(ModelView): 25 | def is_accessible(self): 26 | if current_user.is_authenticated: 27 | current_user.writer = mongo.db.writers.find_one( 28 | {"_id": ObjectId(current_user.id)} 29 | ) 30 | else: 31 | return False 32 | 33 | return current_user.writer["email"] in current_app.config["ADMIN_USER_EMAILS"] 34 | 35 | 36 | class UserForm(form.Form): 37 | confirmed = fields.BooleanField("confirmed") 38 | email = fields.StringField("email") 39 | time = fields.SelectField("time", choices=[i for i in range(24)]) 40 | extras = fields.SelectMultipleField( 41 | "extras", choices=["codingchallenge", "repositories"] 42 | ) 43 | password = fields.StringField("password") 44 | frequency = fields.SelectField( 45 | "frequency", 46 | choices=[ 47 | ([1, 2, 3, 4, 5, 6, 7], "everyday"), 48 | ([1, 2, 3, 4, 5], "weekdays"), 49 | ([6, 7], "weekends"), 50 | ], 51 | ) 52 | theme = fields.SelectField("theme", choices=[("light", "light"), ("dark", "dark")]) 53 | timezone = fields.SelectField("timezone", choices=pytz.all_timezones) 54 | 55 | 56 | class UserView(SecureModelView): 57 | column_list = ( 58 | "email", 59 | "confirmed", 60 | "time", 61 | "extras", 62 | "frequency", 63 | "theme", 64 | "timezone", 65 | ) 66 | 67 | form = UserForm 68 | 69 | 70 | class ArticleForm(form.Form): 71 | title = fields.StringField("title") 72 | description = fields.StringField("description") 73 | content = fields.TextAreaField("content") 74 | thumbnail = fields.StringField("thumbnail") 75 | categories = fields.SelectMultipleField( 76 | "categories", 77 | choices=[ 78 | ("ai-news", "AI"), 79 | ("corporation-news", "Corporation"), 80 | ("crypto-news", "Crypto"), 81 | ("gadget-news", "Gadget"), 82 | ("gaming-news", "Gaming"), 83 | ("robotics-news", "Robotics"), 84 | ("science-news", "Science"), 85 | ("space-news", "Space"), 86 | ("other-news", "Other"), 87 | ], 88 | ) 89 | source = fields.SelectField( 90 | "source", 91 | choices=[ 92 | ("gmt", "GMT"), 93 | ("techcrunch", "TechCrunch"), 94 | ("verge", "TheVerge"), 95 | ("bbc", "BBC"), 96 | ("cnn", "CNN"), 97 | ("guardian", "Guardian"), 98 | ], 99 | ) 100 | formatted_source = fields.SelectField( 101 | "source", 102 | choices=[ 103 | ("GMT", "GMT"), 104 | ("TechCrunch", "TechCrunch"), 105 | ("TheVerge", "TheVerge"), 106 | ("BBC", "BBC"), 107 | ("CNN", "CNN"), 108 | ("Guardian", "Guardian"), 109 | ], 110 | ) 111 | author = fields.StringField("author") 112 | url = fields.StringField("url") 113 | views = fields.IntegerField("views", default=0) 114 | date = fields.DateTimeField("date", default=datetime.datetime.utcnow()) 115 | 116 | 117 | class ArticleView(SecureModelView): 118 | column_list = ( 119 | "title", 120 | "description", 121 | "content", 122 | "thumbnail", 123 | "categories", 124 | "source", 125 | "formatted_source", 126 | "author", 127 | "url", 128 | "views", 129 | "date", 130 | ) 131 | 132 | form = ArticleForm 133 | 134 | 135 | class WriterForm(form.Form): 136 | about = fields.TextAreaField("about") 137 | accepted = fields.BooleanField("accepted") 138 | badges = fields.SelectMultipleField( 139 | "badges", choices=[("dev", "Dev"), ("writer", "Writer"), ("tester", "Tester")] 140 | ) 141 | confirmed = fields.BooleanField("confirmed") 142 | created_at = fields.DateTimeField("created_at") 143 | email = fields.StringField("email") 144 | github = fields.StringField("github") 145 | name = fields.StringField("name") 146 | password = fields.PasswordField("password") 147 | patreon = fields.StringField("patreon") 148 | paypal = fields.StringField("paypal") 149 | public_email = fields.StringField("public_email") 150 | reasoning = fields.TextAreaField("reasoning") 151 | timezone = fields.SelectField("timezone", choices=pytz.all_timezones) 152 | twitter = fields.StringField("twitter") 153 | user_name = fields.StringField("user_name") 154 | views = fields.IntegerField("views") 155 | website = fields.StringField("website") 156 | 157 | 158 | class WriterView(SecureModelView): 159 | column_list = ( 160 | "about", 161 | "accepted", 162 | "badges", 163 | "confirmed", 164 | "created_at", 165 | "email", 166 | "github", 167 | "name", 168 | "password", 169 | "patreon", 170 | "paypal", 171 | "public_email", 172 | "reasoning", 173 | "timezone", 174 | "twitter", 175 | "user_name", 176 | "views", 177 | "website", 178 | ) 179 | 180 | form = WriterForm 181 | 182 | 183 | admin.add_view(UserView(mongo.db.users, "Users")) 184 | admin.add_view(ArticleView(mongo.db.articles, "Articles")) 185 | admin.add_view(WriterView(mongo.db.writers, "Writers")) 186 | -------------------------------------------------------------------------------- /gmt/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import Flask, render_template 4 | from flask_mail import Mail 5 | from flask_pymongo import PyMongo 6 | from flask_turnstile import Turnstile 7 | from pymongo import MongoClient 8 | from flask_wtf.csrf import CSRFProtect 9 | from flask_session import Session 10 | from flask_mde import Mde 11 | from flask_login import LoginManager, UserMixin 12 | from flask_admin import Admin 13 | 14 | try: 15 | from flask_crontab import Crontab 16 | except ImportError: 17 | # @crontab.job(minute="*/30") create dummy cron job, to be crontab, FOR LOCAL DEVELOPMENT 18 | print("Crontab not installed, using dummy crontab") 19 | 20 | class Crontab: 21 | def job(self, **kwargs): 22 | def decorator(func): 23 | return func 24 | 25 | return decorator 26 | 27 | def init_app(self, app): 28 | pass 29 | 30 | 31 | crontab = Crontab() 32 | mail = Mail() 33 | mongo = PyMongo() 34 | csrf = CSRFProtect() 35 | sess = Session() 36 | mde = Mde() 37 | login_manager = LoginManager() 38 | turnstile = Turnstile() 39 | admin = Admin(name="Admin Page", template_mode="bootstrap4") 40 | 41 | 42 | class User(UserMixin): 43 | pass 44 | 45 | 46 | def create_app() -> Flask: 47 | """Create the Flask app. 48 | 49 | This function is responsible for creating the main Flask app, and it's the entry point for the factory pattern. 50 | """ 51 | app = Flask(__name__, instance_relative_config=True) 52 | 53 | load_configuration(app) 54 | init_extensions(app) 55 | register_blueprints(app) 56 | 57 | return app 58 | 59 | 60 | def load_configuration(app: Flask) -> None: 61 | """Load the configuration. 62 | 63 | The configuration will be loaded either from a configuration file or from environment variables. 64 | 65 | The following variables can be configured: 66 | - SECRET_KEY: A secret key used for any security related needs. It should be a long random string. 67 | - SERVER_NAME: Inform the application what host and port it is bound to. 68 | - MONGO_URI: The connection URI to the MongoDB database. It should specify the database name as well. 69 | - MAIL_SERVER: The SMTP server to connect to. 70 | - MAIL_PORT: The port of the SMTP server. 71 | - MAIL_USE_TLS: True if TLS is to be used. 72 | - MAIL_USE_SSL: True if SSL is to be used. 73 | - MAIL_USERNAME: The email address to send the mail from. 74 | - MAIL_PASSWORD: The password of the email address. 75 | - WRITER_WEBHOOK: The URL of the Discord webhook to send writer apply requests. 76 | - FORM_WEBHOOK: The URL of the Discord webhook to send form requests. 77 | """ 78 | app.config["FLASK_ADMIN_SWATCH"] = "lux" 79 | app.config["SESSION_TYPE"] = "mongodb" 80 | app.config["SESSION_MONGODB_DB"] = "goodmorningtech" 81 | app.config["SESSION_MONGODB_COLLECT"] = "sessions" 82 | try: 83 | app.config.from_pyfile("config.py") 84 | app.config["SESSION_MONGODB"] = MongoClient(app.config["MONGO_URI"]) 85 | except OSError: 86 | app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY") 87 | app.config["DOMAIN_NAME"] = os.environ.get("DOMAIN_NAME") 88 | app.config["MONGO_URI"] = os.environ.get("MONGO_URI") 89 | app.config["MAIL_SERVER"] = os.environ.get("MAIL_SERVER") 90 | app.config["MAIL_PORT"] = os.environ.get("MAIL_PORT") 91 | app.config["MAIL_USE_TLS"] = os.environ.get("MAIL_USE_TLS") 92 | app.config["MAIL_USE_SSL"] = os.environ.get("MAIL_USE_SSL") 93 | app.config["MAIL_USERNAME"] = os.environ.get("MAIL_USERNAME") 94 | app.config["MAIL_PASSWORD"] = os.environ.get("MAIL_PASSWORD") 95 | app.config["WRITER_WEBHOOK"] = os.environ.get("WRITER_WEBHOOK") 96 | app.config["FORM_WEBHOOK"] = os.environ.get("FORM_WEBHOOK") 97 | app.config["CRON_JOB_WEBHOOK"] = os.environ.get("CRON_JOB_WEBHOOK") 98 | app.config["OPENAI_API_KEY"] = os.environ.get("OPENAI_API_KEY") 99 | app.config["FTP_USER"] = os.environ.get("FTP_USER") 100 | app.config["FTP_PASSWORD"] = os.environ.get("FTP_PASSWORD") 101 | app.config["FTP_HOST"] = os.environ.get("FTP_HOST") 102 | app.config["API_NINJA_KEY"] = os.environ.get("API_NINJA_KEY") 103 | app.config["MISTRAL_API_KEY"] = os.environ.get("MISTRAL_API_KEY") 104 | app.config["ADMIN_USER_EMAILS"] = ( 105 | os.environ.get("ADMIN_USER_EMAILS").split(",") 106 | if os.environ.get("ADMIN_USER_EMAILS") 107 | else [] 108 | ) 109 | app.config["SESSION_MONGODB"] = MongoClient(app.config["MONGO_URI"]) 110 | 111 | app.config["TURNSTILE_SITE_KEY"] = os.environ.get("TURNSTILE_SITE_KEY") 112 | app.config["TURNSTILE_SECRET_KEY"] = os.environ.get("TURNSTILE_SECRET_KEY") 113 | 114 | if app.config["MAIL_PORT"]: 115 | app.config["MAIL_PORT"] = int(app.config["MAIL_PORT"]) 116 | if app.config["MAIL_USE_TLS"]: 117 | app.config["MAIL_USE_TLS"] = app.config["MAIL_USE_TLS"].casefold() == "true" 118 | if app.config["MAIL_USE_SSL"]: 119 | app.config["MAIL_USE_SSL"] = app.config["MAIL_USE_SSL"].casefold() == "true" 120 | 121 | 122 | def init_extensions(app: Flask) -> None: 123 | """Initialize Flask extensions.""" 124 | csrf.init_app(app) 125 | mail.init_app(app) 126 | mongo.init_app(app) 127 | sess.init_app(app) 128 | mde.init_app(app) 129 | login_manager.init_app(app) 130 | admin.init_app(app) 131 | crontab.init_app(app) 132 | turnstile.init_app(app) 133 | 134 | 135 | def register_blueprints(app: Flask) -> None: 136 | """Register Flask blueprints.""" 137 | from .views import articles, auth, commands, general, writers, admin, api 138 | 139 | @app.errorhandler(404) 140 | def page_not_found(_): 141 | return render_template("404.html") 142 | 143 | app.register_blueprint(articles.bp) 144 | app.register_blueprint(auth.bp) 145 | app.register_blueprint(commands.bp) 146 | app.register_blueprint(general.bp) 147 | app.register_blueprint(writers.bp) 148 | app.register_blueprint(admin.bp) 149 | app.register_blueprint(api.bp) 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Good Morning Tech 2 |
3 | 4 |
5 |

We are an open-source tech newsletter, sign up and stay updated with the latest news in tech at your convenience! Oh did I mention, we are 100% free?

6 | Checkout our websiteGet in touch with usReport a bug 7 |
8 |
9 | 10 |
11 | 12 | ![Project Details](https://img.shields.io/github/repo-size/goodmornintech/goodmorningtech?color=red&label=Project%20Size&style=for-the-badge) 13 | ![License](https://img.shields.io/github/license/goodmornintech/goodmorningtech?color=red&style=for-the-badge) 14 | ![Stars](https://img.shields.io/github/stars/goodmornintech/goodmorningtech?color=red&label=Project%20Stars&style=for-the-badge) 15 | ![Contributors](https://img.shields.io/github/contributors/goodmornintech/goodmorningtech?color=red&style=for-the-badge) 16 | 17 | 18 | ![Status](https://uptime.goodmorningtech.news/api/badge/1/status?style=for-the-badge) 19 | ![Uptime](https://uptime.goodmorningtech.news/api/badge/1/uptime/24?style=for-the-badge) 20 | ![Uptime](https://uptime.goodmorningtech.news/api/badge/1/ping/1000?style=for-the-badge) 21 | 22 |
23 |
24 |
25 |
26 | 27 |
28 | 29 |
30 | Table of Content: 31 |
    32 |
  1. 33 | Learn more about this project 34 | 38 |
  2. 39 |
  3. 40 | Get started 41 | 45 |
  4. 46 |
  5. What's planned ahead
  6. 47 |
  7. Frequently Asked Question's (FAQs)
  8. 48 |
  9. License
  10. 49 |
  11. Contact Us
  12. 50 |
  13. Our team
  14. 51 |
52 |
53 | 54 | 55 | ## Learn more about this project 56 | 57 | Mockup of the website 58 | 59 | 60 | ### Built With 61 | ![Figma](https://cdn.goodmorningtech.news/README/badges/Figma.svg) 62 | ![Adobe Photoshop](https://cdn.goodmorningtech.news/README/badges/AdobePhotoshop.svg) 63 | ![Dribbble](https://cdn.goodmorningtech.news/README/badges/Dribble.svg) 64 |
65 | ![Python](https://cdn.goodmorningtech.news/README/badges/Python.svg) 66 | ![HTML5](https://cdn.goodmorningtech.news/README/badges/HTML5.svg) 67 | ![CSS3](https://cdn.goodmorningtech.news/README/badges/CSS3.svg) 68 | ![JavaScript](https://cdn.goodmorningtech.news/README/badges/JavaScript.svg) 69 |
70 | ![NPM](https://cdn.goodmorningtech.news/README/badges/NPM.svg) 71 | ![Flask](https://cdn.goodmorningtech.news/README/badges/Flask.svg) 72 | ![TailwindCSS](https://cdn.goodmorningtech.news/README/badges/TailwindCSS.svg) 73 |
74 | ![MongoDB](https://cdn.goodmorningtech.news/README/badges/MongoDB.svg) 75 | ![Vercel](https://cdn.goodmorningtech.news/README/badges/Vercel.svg) 76 | ![Cybrancee](https://cdn.goodmorningtech.news/README/badges/Cybrancee.svg) 77 |
78 | 79 | 80 | ### Features 81 | 82 | - Timezone Selection 83 | - Day and time Selection 84 | - Article Count Selection 85 | 86 | ## Get started 87 | ### Contribute 88 | Contributing to this project is quite simple & straight forward. We'd request you to view our [contributing](https://github.com/GoodMorninTech/GoodMorningTech/blob/master/CONTRIBUTING.md) file before getting started and follow our [code of conduct](https://github.com/GoodMorninTech/GoodMorningTech/blob/master/CODE_OF_CONDUCT.md). 89 | 90 | ### Setting up on your local machine 91 | [Check out this guide](https://github.com/GoodMorninTech/GoodMorningTech/blob/master/CONTRIBUTING.md#getting-started) 92 | 93 | ## What's Planned Ahead: 94 | - [x] Time Selection 95 | - [x] Timezone Selection 96 | - [x] Addition of more news sources 97 | - [x] Blogging System 98 | - [ ] Changelog System 99 | - [ ] Support for Other Languages 100 | - [ ] French 101 | - [ ] German 102 | - [ ] Spanish 103 | - [ ] Mobile App 104 | 105 | ## Frequently Asked Question's (FAQs): 106 | 107 | #### 1. How does this work? 108 | 109 | It gets the important posts from BBC, The Guardian, Verge & other credible sources and sends them to your email. 110 | 111 | #### 2. How do I subscribe? 112 | 113 | Subscribing is as easy as heading to our [sign up page](https://goodmorningtech.news/subscribe) and giving us your email & filling a small form (we promise we won't flood your inbox). 114 | 115 | #### 3. How do I unsubscribe? 116 | 117 | We hate to see you leave, you can head to [this page](https://goodmorningtech.news/unsubscribe) and enter your email ID, we'll then send you a link to verify your exit. Alternatively, each newsletter we send you has a footer with an unsubscribe link. 118 | 119 | #### 4. How do you guys fund your project if it's completely free? 120 | We rely on donations/sponsors! 121 | 122 | ## License 123 | 124 | [MIT](https://choosealicense.com/licenses/mit/) 125 | 126 | 127 | ## Contact Us 128 | ![Twitter](https://cdn.goodmorningtech.news/README/badges/Twitter.svg) 129 | ![Instagram](https://cdn.goodmorningtech.news/README/badges/Instagram.svg) 130 | ![Discord](https://cdn.goodmorningtech.news/README/badges/Discord.svg) 131 | 132 | 133 | ## Our team 134 | - [OpenSourceSimon](https://github.com/OpenSourceSimon) - Backend 135 | - [Kappq](https://github.com/kappq) - Backend 136 | - [ImmaHarry](https://github.com/immaharry) - Site Designer & Frontend 137 | - [LevaniVashadze](https://github.com/LevaniVashadze) - Backend & Frontend 138 | -------------------------------------------------------------------------------- /gmt/templates/writers/portal.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Portal{% endblock %} 3 | {% block body %} 4 |
5 |
6 |
7 |

Writer Portal

8 |

Here you can write your own articles.

9 | 10 | {% if profile_picture %} 11 | Profile picture 14 | {% else %} 15 | 19 | 21 | 22 | {% endif %} 23 | 24 | 72 |
73 |
74 |

Articles

75 |

76 | Click here to write a new article. 77 |

78 |
    79 | {% for article in articles %} 80 |
  • 81 | - {{ article.title }} 83 |
  • 84 | {% endfor %} 85 |
86 |
87 |
88 |
89 | {% endblock %} 90 | -------------------------------------------------------------------------------- /gmt/templates/general/morning.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Morning{% endblock %} 3 | {% block head %} 4 | 6 | 8 | 9 | 10 | 11 | 13 | 14 | 16 | 17 | {% endblock %} 18 | {% block body %} 19 | 35 |
36 |
38 |
39 |

40 | Get tech news to your 41 | Discord Community 42 |

43 |

44 | Get the latest tech news straight to your Discord 45 | community on a channel of your choice! Get all of the customizing options offered on the email 46 | newsletter using our open-source Discord Bot, 47 | Morning! 48 |

49 | 59 |
60 | 65 | 70 |
71 |
73 |

74 | 75 | How to set it up: 76 | 77 |

78 |
79 |
80 | 81 | Invite bot to server 83 |
84 |
85 |

86 | Inviting Morning 87 |

88 |

89 | Invite Morning, our official Discord bot to your Discord server by clicking the button below, this should take you 5 seconds! 90 |

91 | 93 | Invite Morning 94 | 95 |
96 |
97 | 98 | Invite bot to server 100 |
101 |
102 |

103 | Setting up Morning 104 |

105 |

106 | Run the 107 | /setup create command to setup the bot on your server, select everything you wish the bot should deliver! 108 |

109 |
110 |
111 |
112 |
113 | {% endblock %} 114 | -------------------------------------------------------------------------------- /gmt/templates/general/contact.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Contact Us{% endblock %} 3 | {% block head %} 4 | 6 | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 17 | 18 | 19 | {% endblock %} 20 | {% block body %} 21 | {# #} 29 |
30 |
31 |

33 | Get in Touch! 34 |

35 |

36 | Have any queries? Fill out this small form & we’ll get back to you as soon as we can! 37 |

38 | 39 | 40 | support@goodmorningtech.news 41 | 42 |
43 |

Get in touch through:

44 |
45 | 46 | 47 | 48 |
49 | 50 | 51 | 52 |
53 |
54 |
55 |
56 |
59 | 60 |
61 | {% if error %} 62 | 67 | {% endif %} 68 | {% if success %} 69 | 74 | {% endif %} 75 | 76 |
77 | 83 |
84 |
85 | 86 |
87 | 93 |
94 |
95 | 96 |
97 | 103 |
104 | 111 | 112 | 115 |
116 |
117 | 121 |
122 |
123 | 126 |
127 |
128 | {% endblock %} 129 | -------------------------------------------------------------------------------- /gmt/views/general.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import random 3 | import re 4 | 5 | from bson import ObjectId 6 | from email_validator import validate_email, EmailNotValidError 7 | from flask import Blueprint, render_template, redirect, request, url_for, current_app 8 | from flask_mail import Message 9 | from werkzeug import Response 10 | from markdown import markdown 11 | from flask_login import login_required, current_user 12 | 13 | from ..news import get_news 14 | from .. import mongo, login_manager, User, mail 15 | from ..utils import random_language_greeting 16 | from ..extras import get_daily_coding_challenge, get_trending_repos, get_surprise 17 | 18 | bp = Blueprint("general", __name__) 19 | 20 | 21 | @bp.route("/", methods=["GET", "POST"]) 22 | def index(): 23 | """Render the home page.""" 24 | if request.method == "POST": 25 | email = request.form.get("email") 26 | if email: 27 | return redirect(url_for("auth.subscribe", email=email)) 28 | 29 | if current_user.is_authenticated: 30 | current_user.writer = mongo.db.writers.find_one( 31 | {"_id": ObjectId(current_user.id)} 32 | ) 33 | 34 | posts = mongo.db.articles.find( 35 | {"date": {"$gte": datetime.datetime.utcnow() - datetime.timedelta(hours=25)}} 36 | ) 37 | 38 | # Mix the posts 39 | posts = list(posts) 40 | if len(posts) > 2: 41 | # Gets a random post and removes it from the list 42 | post1 = random.choice(posts) 43 | posts.remove(post1) 44 | # Gets a second random post 45 | post2 = random.choice(posts) 46 | 47 | # set limits for the description to 360 characters and add more length for each [link] tag 48 | limit1 = 360 + post1["description"][:360].count("[link]") * 30 49 | limit2 = 360 + post2["description"][:360].count("[link]") * 30 50 | 51 | # slice the description to the limit 52 | post1["description"] = post1["description"][:limit1] 53 | post2["description"] = post2["description"][:limit2] 54 | 55 | # remove unterminated [link] tags 56 | if re.search("\[link\]\([^\)]*[^\)]$", post1["description"]): 57 | post1["description"] = re.sub( 58 | "\[link\]\(.*[^\)]$", "", post1["description"] 59 | ) 60 | if re.search("\[link\]\([^\)]*[^\)]$", post2["description"]): 61 | post2["description"] = re.sub( 62 | "\[link\]\([^\)]*[^\)]$", "", post2["description"] 63 | ) 64 | 65 | # add ellipses and markdown it 66 | post1["description"] = markdown(post1["description"] + "...") 67 | post2["description"] = markdown(post2["description"] + "...") 68 | posts = [post1, post2] 69 | else: 70 | posts = [] 71 | 72 | return render_template("general/index.html", news=posts) 73 | 74 | 75 | @bp.route("/news") 76 | def news(): 77 | """Render the newspaper.""" 78 | posts = list( 79 | mongo.db.articles.find( 80 | { 81 | "date": { 82 | "$gte": datetime.datetime.utcnow() - datetime.timedelta(hours=25) 83 | } 84 | } 85 | ) 86 | ) 87 | 88 | if not posts: 89 | posts = get_news(choice="BBC") 90 | 91 | random.shuffle(posts) 92 | 93 | return render_template( 94 | "general/news.html", 95 | posts=posts[:12], 96 | theme="light", 97 | markdown=markdown, 98 | domain_name=current_app.config["DOMAIN_NAME"], 99 | repos=get_trending_repos(), 100 | coding_challenge=get_daily_coding_challenge(), 101 | surprise=get_surprise(), 102 | random_language_greeting=random_language_greeting(), 103 | ) 104 | 105 | 106 | @bp.route("/about") 107 | def about(): 108 | if current_user.is_authenticated: 109 | current_user.writer = mongo.db.writers.find_one( 110 | {"_id": ObjectId(current_user.id)} 111 | ) 112 | return render_template("general/about.html", no_meta=True) 113 | 114 | 115 | @bp.route("/contact", methods=["GET", "POST"]) 116 | def contact(): 117 | if current_user.is_authenticated: 118 | current_user.writer = mongo.db.writers.find_one( 119 | {"_id": ObjectId(current_user.id)} 120 | ) 121 | if request.method == "POST": 122 | fake_email = request.form.get("email") 123 | if fake_email: 124 | return render_template( 125 | "general/contact.html", 126 | error="Invalid email address", 127 | success=False, 128 | no_meta=True, 129 | ) 130 | email = request.form.get("real_email") 131 | name = request.form.get("name") 132 | subject = request.form.get("subject") 133 | message = request.form.get("message") 134 | try: 135 | validate_email(email) 136 | except EmailNotValidError as e: 137 | return render_template( 138 | "general/contact.html", 139 | error="Invalid email address", 140 | success=False, 141 | no_meta=True, 142 | ) 143 | else: 144 | msg = Message( 145 | subject=f"Contact Form Submission from {name} - {subject}", 146 | sender=("Good Morning Tech", current_app.config["MAIL_USERNAME"]), 147 | recipients=["support@goodmorningtech.news"], 148 | body=f"From: {name} <{email}>,\n{message}", 149 | ) 150 | mail.send(msg) 151 | return render_template( 152 | "general/contact.html", success=True, error=None, no_meta=True 153 | ) 154 | if current_user.is_authenticated: 155 | current_user.writer = mongo.db.writers.find_one( 156 | {"_id": ObjectId(current_user.id)} 157 | ) 158 | return render_template( 159 | "general/contact.html", success=False, error=None, no_meta=True 160 | ) 161 | 162 | 163 | @bp.route("/contribute") 164 | def contribute(): 165 | if current_user.is_authenticated: 166 | current_user.writer = mongo.db.writers.find_one( 167 | {"_id": ObjectId(current_user.id)} 168 | ) 169 | return render_template("general/contribute.html") 170 | 171 | 172 | @bp.route("/morning") 173 | def morning(): 174 | if current_user.is_authenticated: 175 | current_user.writer = mongo.db.writers.find_one( 176 | {"_id": ObjectId(current_user.id)} 177 | ) 178 | return render_template("general/morning.html", no_meta=True) 179 | 180 | 181 | @bp.route("/privacy") 182 | def privacy(): 183 | if current_user.is_authenticated: 184 | current_user.writer = mongo.db.writers.find_one( 185 | {"_id": ObjectId(current_user.id)} 186 | ) 187 | return render_template("general/privacy_policy.html") 188 | 189 | 190 | @bp.route("/tos") 191 | def terms(): 192 | if current_user.is_authenticated: 193 | current_user.writer = mongo.db.writers.find_one( 194 | {"_id": ObjectId(current_user.id)} 195 | ) 196 | return render_template("general/tos.html") 197 | 198 | 199 | @bp.route("/credits") 200 | def credits(): 201 | if current_user.is_authenticated: 202 | current_user.writer = mongo.db.writers.find_one( 203 | {"_id": ObjectId(current_user.id)} 204 | ) 205 | return render_template("general/credits.html") 206 | 207 | 208 | @bp.route("/sitemap.xml") 209 | def sitemap(): 210 | """Render the sitemap.xml.""" 211 | 212 | sitemap_xml = render_template("general/sitemap.xml") 213 | response = Response(sitemap_xml, mimetype="text/xml") 214 | response.headers["Content-Type"] = "application/xml" 215 | 216 | return response 217 | 218 | 219 | @bp.route("/robots.txt") 220 | def robots(): 221 | """Render the robots.txt.""" 222 | robots_txt = render_template("general/robots.txt") 223 | response = Response(robots_txt, mimetype="text/plain") 224 | response.headers["Content-Type"] = "text/plain" 225 | 226 | return response 227 | 228 | 229 | @login_manager.user_loader 230 | def load_user(user_id): 231 | user_doc = mongo.db.writers.find_one({"_id": ObjectId(user_id)}) 232 | if user_doc: 233 | user = User() 234 | user.id = str(user_doc["_id"]) 235 | return user 236 | else: 237 | return None 238 | 239 | 240 | @login_manager.unauthorized_handler 241 | def unauthorized_callback(): 242 | return redirect(url_for("writers.login")) 243 | -------------------------------------------------------------------------------- /gmt/extras.py: -------------------------------------------------------------------------------- 1 | import random 2 | import bs4 3 | import requests 4 | from flask import current_app 5 | 6 | from .utils import format_html 7 | 8 | 9 | def filter_articles(raw_html: str) -> str: 10 | """Filters HTML out, which is not enclosed by article-tags. 11 | Beautifulsoup is inaccurate and slow when applied on a larger 12 | HTML string, this filtration fixes this. 13 | """ 14 | raw_html_lst = raw_html.split("\n") 15 | 16 | # count number of article tags within the document (varies from 0 to 50): 17 | article_tags_count = 0 18 | tag = "article" 19 | for line in raw_html_lst: 20 | if tag in line: 21 | article_tags_count += 1 22 | 23 | # copy HTML enclosed by first and last article-tag: 24 | articles_arrays, is_article = [], False 25 | for line in raw_html_lst: 26 | if tag in line: 27 | article_tags_count -= 1 28 | is_article = True 29 | if is_article: 30 | articles_arrays.append(line) 31 | if not article_tags_count: 32 | is_article = False 33 | return "".join(articles_arrays) 34 | 35 | 36 | def make_soup(articles_html: str) -> bs4.element.ResultSet: 37 | """HTML enclosed by article-tags is converted into a 38 | soup for further data extraction. 39 | """ 40 | soup = bs4.BeautifulSoup(articles_html, "lxml") 41 | return soup.find_all("article", class_="Box-row") 42 | 43 | 44 | def scraping_repositories( 45 | matches: bs4.element.ResultSet, 46 | since: str, 47 | ): 48 | """Data about all trending repositories are extracted.""" 49 | trending_repositories = [] 50 | for rank, match in enumerate(matches): 51 | # description 52 | if match.p: 53 | description = match.p.get_text(strip=True) 54 | else: 55 | description = None 56 | 57 | # relative url: 58 | rel_url = match.select_one("h2 > a")["href"] 59 | 60 | # absolute url: 61 | repo_url = "https://github.com" + rel_url 62 | 63 | # name of repo 64 | repository_name = rel_url.split("/")[-1] 65 | 66 | # author (username): 67 | username = rel_url.split("/")[-2] 68 | 69 | # language and color 70 | progr_language = match.find("span", itemprop="programmingLanguage") 71 | if progr_language: 72 | language = progr_language.get_text(strip=True) 73 | lang_color_tag = match.find("span", class_="repo-language-color") 74 | lang_color = lang_color_tag["style"].split()[-1] 75 | else: 76 | lang_color, language = None, None 77 | 78 | stars_built_section = match.div.findNextSibling("div") 79 | 80 | # total stars: 81 | if stars_built_section.a: 82 | raw_total_stars = stars_built_section.a.get_text(strip=True) 83 | if "," in raw_total_stars: 84 | raw_total_stars = raw_total_stars.replace(",", "") 85 | if raw_total_stars: 86 | total_stars: int 87 | try: 88 | total_stars = int(raw_total_stars) 89 | except ValueError as missing_number: 90 | print(missing_number) 91 | else: 92 | total_stars = None 93 | 94 | # forks 95 | if stars_built_section.a.findNextSibling("a"): 96 | raw_forks = stars_built_section.a.findNextSibling( 97 | "a", 98 | ).get_text(strip=True) 99 | if "," in raw_forks: 100 | raw_forks = raw_forks.replace(",", "") 101 | if raw_forks: 102 | forks: int 103 | try: 104 | forks = int(raw_forks) 105 | except ValueError as missing_number: 106 | print(missing_number) 107 | else: 108 | forks = None 109 | 110 | # stars in period 111 | if stars_built_section.find( 112 | "span", 113 | class_="d-inline-block float-sm-right", 114 | ): 115 | raw_stars_since = ( 116 | stars_built_section.find( 117 | "span", 118 | class_="d-inline-block float-sm-right", 119 | ) 120 | .get_text(strip=True) 121 | .split()[0] 122 | ) 123 | if "," in raw_stars_since: 124 | raw_stars_since = raw_stars_since.replace(",", "") 125 | if raw_stars_since: 126 | stars_since: int 127 | try: 128 | stars_since = int(raw_stars_since) 129 | except ValueError as missing_number: 130 | print(missing_number) 131 | else: 132 | stars_since = None 133 | 134 | # builtby 135 | built_section = stars_built_section.find( 136 | "span", 137 | class_="d-inline-block mr-3", 138 | ) 139 | if built_section: 140 | contributors = stars_built_section.find( 141 | "span", 142 | class_="d-inline-block mr-3", 143 | ).find_all("a") 144 | built_by = [] 145 | for contributor in contributors: 146 | contr_data = {} 147 | contr_data["username"] = contributor["href"].strip("/") 148 | contr_data["url"] = "https://github.com" + contributor["href"] 149 | contr_data["avatar"] = contributor.img["src"] 150 | built_by.append(dict(contr_data)) 151 | else: 152 | built_by = None 153 | 154 | repositories = { 155 | "rank": rank + 1, 156 | "username": username, 157 | "name": repository_name, 158 | "whole_name": username + "/" + repository_name, 159 | "url": repo_url, 160 | "description": description, 161 | "language": language, 162 | "language_color": lang_color, 163 | "total_stars": total_stars, 164 | "forks": forks, 165 | "stars_since": stars_since, 166 | "since": since, 167 | "built_by": built_by, 168 | } 169 | trending_repositories.append(repositories) 170 | 171 | return trending_repositories 172 | 173 | 174 | def get_trending_repos(since="daily"): 175 | payload = {"since": since} # "daily", "weekly", "monthly", "yearly" 176 | 177 | url = "https://github.com/trending" 178 | raw_html = requests.get(url, params=payload).text 179 | 180 | articles_html = filter_articles(raw_html) 181 | soup = make_soup(articles_html) 182 | trending_repos = scraping_repositories(soup, since=payload["since"]) 183 | 184 | return trending_repos[:4] 185 | 186 | 187 | def get_daily_coding_challenge(): 188 | headers = { 189 | "Content-Type": "application/json", 190 | } 191 | 192 | json_data = { 193 | "query": "query questionOfToday {\n\tactiveDailyCodingChallengeQuestion {\n\t\tdate\n\t\tuserStatus\n\t\tlink\n\t\tquestion {\n\t\t\tacRate\n\t\t\tdifficulty\n\t\t\tfreqBar\n\t\t\tfrontendQuestionId: questionFrontendId\n\t\t\tisFavor\n\t\t\tpaidOnly: isPaidOnly\n\t\t\tstatus\n\t\t\ttitle\n\t\t\ttitleSlug\n\t\t\thasVideoSolution\n\t\t\thasSolution\n\t\t\ttopicTags {\n\t\t\t\tname\n\t\t\t\tid\n\t\t\t\tslug\n\t\t\t}\n\t\t}\n\t}\n}\n", 194 | "operationName": "questionOfToday", 195 | } 196 | 197 | response = requests.post( 198 | "https://leetcode.com/graphql", headers=headers, json=json_data 199 | ) 200 | json_response = response.json() 201 | title_slug = json_response["data"]["activeDailyCodingChallengeQuestion"][ 202 | "question" 203 | ]["titleSlug"] 204 | 205 | json_data = { 206 | "query": "\n query questionContent($titleSlug: String!) {\n question(titleSlug: $titleSlug) {\n content\n mysqlSchemas\n }\n}\n ", 207 | "variables": {"titleSlug": title_slug}, 208 | "operationName": "questionContent", 209 | } 210 | 211 | response = requests.post( 212 | "https://leetcode.com/graphql", headers=headers, json=json_data 213 | ) 214 | json_response = response.json() 215 | 216 | title = " ".join(word.capitalize() for word in title_slug.split("-")) 217 | raw_content = json_response["data"]["question"]["content"] 218 | description = ( 219 | format_html(raw_content).replace("
", "

").replace("

", "

") 220 | ) 221 | return {"title": title, "description": description} 222 | 223 | 224 | def get_surprise(): 225 | randomizer = random.randint(0, 2) 226 | try: 227 | if randomizer == 0: 228 | joke = requests.get( 229 | "https://v2.jokeapi.dev/joke/Programming,Miscellaneous,Pun?blacklistFlags=nsfw,religious,racist,sexist,explicit" 230 | ).json() 231 | if joke["type"] == "single": 232 | return "Today's joke:\n" + joke["joke"] 233 | else: 234 | return "Today's joke:\n" + joke["setup"] + "\n" + joke["delivery"] 235 | elif randomizer == 1: 236 | quote = requests.get("https://api.quotable.io/quotes/random").json()[0] 237 | return "Today's quote:\n" + quote["content"] + "\n-" + quote["author"] 238 | else: 239 | api_url = "https://api.api-ninjas.com/v1/facts?limit=1" 240 | headers = { 241 | "X-Api-Key": current_app.config["API_NINJA_KEY"], 242 | "Accept": "application/json", 243 | } 244 | response = requests.get(api_url, headers=headers) 245 | fact = response.json() 246 | return "Today's Fact:\n" + fact[0]["fact"] 247 | except Exception as e: 248 | print(e) 249 | return "Sorry, I couldn't get a surprise for you today :( If this occurs again, please contact us." 250 | -------------------------------------------------------------------------------- /gmt/static/loader.css: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Generated by SVG Artista on 11/16/2022, 7:47:19 PM 3 | * MIT license (https://opensource.org/licenses/MIT) 4 | * W. https://svgartista.net 5 | **************************************************/ 6 | 7 | @-webkit-keyframes animate-svg-stroke-1 { 8 | 0% { 9 | stroke-dashoffset: 396.869384765625px; 10 | stroke-dasharray: 396.869384765625px; 11 | } 12 | 13 | 100% { 14 | stroke-dashoffset: 0; 15 | stroke-dasharray: 396.869384765625px; 16 | } 17 | } 18 | 19 | @keyframes animate-svg-stroke-1 { 20 | 0% { 21 | stroke-dashoffset: 396.869384765625px; 22 | stroke-dasharray: 396.869384765625px; 23 | } 24 | 25 | 100% { 26 | stroke-dashoffset: 0; 27 | stroke-dasharray: 396.869384765625px; 28 | } 29 | } 30 | 31 | .gmt-main-icon-1 { 32 | -webkit-animation: animate-svg-stroke-1 1s ease-in 0s both, 33 | animate-svg-fill-1 0.75s cubic-bezier(0.23, 1, 0.32, 1) 0.8s both; 34 | animation: animate-svg-stroke-1 1s ease-in 0s both, 35 | animate-svg-fill-1 0.75s cubic-bezier(0.23, 1, 0.32, 1) 0.8s both; 36 | } 37 | 38 | @-webkit-keyframes animate-svg-stroke-2 { 39 | 0% { 40 | stroke-dashoffset: 334.7112731933594px; 41 | stroke-dasharray: 334.7112731933594px; 42 | } 43 | 44 | 100% { 45 | stroke-dashoffset: 0; 46 | stroke-dasharray: 334.7112731933594px; 47 | } 48 | } 49 | 50 | @keyframes animate-svg-stroke-2 { 51 | 0% { 52 | stroke-dashoffset: 334.7112731933594px; 53 | stroke-dasharray: 334.7112731933594px; 54 | } 55 | 56 | 100% { 57 | stroke-dashoffset: 0; 58 | stroke-dasharray: 334.7112731933594px; 59 | } 60 | } 61 | 62 | @-webkit-keyframes animate-svg-fill-2 { 63 | 0% { 64 | fill: transparent; 65 | } 66 | 67 | 100% { 68 | fill: rgb(207, 51, 51); 69 | } 70 | } 71 | 72 | @keyframes animate-svg-fill-2 { 73 | 0% { 74 | fill: transparent; 75 | } 76 | 77 | 100% { 78 | fill: rgb(207, 51, 51); 79 | } 80 | } 81 | 82 | .gmt-main-icon-2 { 83 | -webkit-animation: animate-svg-stroke-2 1s ease-in 0.12s both, 84 | animate-svg-fill-2 0.75s cubic-bezier(0.23, 1, 0.32, 1) 0.9s both; 85 | animation: animate-svg-stroke-2 1s ease-in 0.12s both, 86 | animate-svg-fill-2 0.75s cubic-bezier(0.23, 1, 0.32, 1) 0.9s both; 87 | } 88 | 89 | @-webkit-keyframes animate-svg-stroke-3 { 90 | 0% { 91 | stroke-dashoffset: 341.52288818359375px; 92 | stroke-dasharray: 341.52288818359375px; 93 | } 94 | 95 | 100% { 96 | stroke-dashoffset: 0; 97 | stroke-dasharray: 341.52288818359375px; 98 | } 99 | } 100 | 101 | @keyframes animate-svg-stroke-3 { 102 | 0% { 103 | stroke-dashoffset: 341.52288818359375px; 104 | stroke-dasharray: 341.52288818359375px; 105 | } 106 | 107 | 100% { 108 | stroke-dashoffset: 0; 109 | stroke-dasharray: 341.52288818359375px; 110 | } 111 | } 112 | 113 | .gmt-main-icon-3 { 114 | -webkit-animation: animate-svg-stroke-3 1s ease-in 0.24s both, 115 | animate-svg-fill-3 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1s both; 116 | animation: animate-svg-stroke-3 1s ease-in 0.24s both, 117 | animate-svg-fill-3 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1s both; 118 | } 119 | 120 | @-webkit-keyframes animate-svg-stroke-4 { 121 | 0% { 122 | stroke-dashoffset: 90.84814453125px; 123 | stroke-dasharray: 90.84814453125px; 124 | } 125 | 126 | 100% { 127 | stroke-dashoffset: 0; 128 | stroke-dasharray: 90.84814453125px; 129 | } 130 | } 131 | 132 | @keyframes animate-svg-stroke-4 { 133 | 0% { 134 | stroke-dashoffset: 90.84814453125px; 135 | stroke-dasharray: 90.84814453125px; 136 | } 137 | 138 | 100% { 139 | stroke-dashoffset: 0; 140 | stroke-dasharray: 90.84814453125px; 141 | } 142 | } 143 | 144 | .gmt-main-icon-4 { 145 | -webkit-animation: animate-svg-stroke-4 1s ease-in 0.36s both, 146 | animate-svg-fill-4 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1.1s both; 147 | animation: animate-svg-stroke-4 1s ease-in 0.36s both, 148 | animate-svg-fill-4 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1.1s both; 149 | } 150 | 151 | @-webkit-keyframes animate-svg-stroke-5 { 152 | 0% { 153 | stroke-dashoffset: 90.84815216064453px; 154 | stroke-dasharray: 90.84815216064453px; 155 | } 156 | 157 | 100% { 158 | stroke-dashoffset: 0; 159 | stroke-dasharray: 90.84815216064453px; 160 | } 161 | } 162 | 163 | @keyframes animate-svg-stroke-5 { 164 | 0% { 165 | stroke-dashoffset: 90.84815216064453px; 166 | stroke-dasharray: 90.84815216064453px; 167 | } 168 | 169 | 100% { 170 | stroke-dashoffset: 0; 171 | stroke-dasharray: 90.84815216064453px; 172 | } 173 | } 174 | 175 | .gmt-main-icon-5 { 176 | -webkit-animation: animate-svg-stroke-5 1s ease-in 0.48s both, 177 | animate-svg-fill-5 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1.2000000000000002s both; 178 | animation: animate-svg-stroke-5 1s ease-in 0.48s both, 179 | animate-svg-fill-5 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1.2000000000000002s both; 180 | } 181 | 182 | @-webkit-keyframes animate-svg-stroke-6 { 183 | 0% { 184 | stroke-dashoffset: 90.84815979003906px; 185 | stroke-dasharray: 90.84815979003906px; 186 | } 187 | 188 | 100% { 189 | stroke-dashoffset: 0; 190 | stroke-dasharray: 90.84815979003906px; 191 | } 192 | } 193 | 194 | @keyframes animate-svg-stroke-6 { 195 | 0% { 196 | stroke-dashoffset: 90.84815979003906px; 197 | stroke-dasharray: 90.84815979003906px; 198 | } 199 | 200 | 100% { 201 | stroke-dashoffset: 0; 202 | stroke-dasharray: 90.84815979003906px; 203 | } 204 | } 205 | 206 | .gmt-main-icon-6 { 207 | -webkit-animation: animate-svg-stroke-6 1s ease-in 0.6s both, 208 | animate-svg-fill-6 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1.3s both; 209 | animation: animate-svg-stroke-6 1s ease-in 0.6s both, 210 | animate-svg-fill-6 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1.3s both; 211 | } 212 | 213 | @-webkit-keyframes animate-svg-stroke-7 { 214 | 0% { 215 | stroke-dashoffset: 959.854736328125px; 216 | stroke-dasharray: 959.854736328125px; 217 | } 218 | 219 | 100% { 220 | stroke-dashoffset: 0; 221 | stroke-dasharray: 959.854736328125px; 222 | } 223 | } 224 | 225 | @keyframes animate-svg-stroke-7 { 226 | 0% { 227 | stroke-dashoffset: 959.854736328125px; 228 | stroke-dasharray: 959.854736328125px; 229 | } 230 | 231 | 100% { 232 | stroke-dashoffset: 0; 233 | stroke-dasharray: 959.854736328125px; 234 | } 235 | } 236 | 237 | @-webkit-keyframes animate-svg-fill-7 { 238 | 0% { 239 | fill: transparent; 240 | } 241 | 242 | 100% { 243 | fill: rgb(0, 0, 0); 244 | } 245 | } 246 | 247 | @keyframes animate-svg-fill-7 { 248 | 0% { 249 | fill: transparent; 250 | } 251 | 252 | 100% { 253 | fill: rgb(0, 0, 0); 254 | } 255 | } 256 | 257 | .gmt-main-icon-7 { 258 | -webkit-animation: animate-svg-stroke-7 1s ease-in 0.72s both, 259 | animate-svg-fill-7 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1.4000000000000001s both; 260 | animation: animate-svg-stroke-7 1s ease-in 0.72s both, 261 | animate-svg-fill-7 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1.4000000000000001s both; 262 | } 263 | 264 | @-webkit-keyframes animate-svg-stroke-8 { 265 | 0% { 266 | stroke-dashoffset: 328px; 267 | stroke-dasharray: 328px; 268 | } 269 | 270 | 100% { 271 | stroke-dashoffset: 0; 272 | stroke-dasharray: 328px; 273 | } 274 | } 275 | 276 | @keyframes animate-svg-stroke-8 { 277 | 0% { 278 | stroke-dashoffset: 328px; 279 | stroke-dasharray: 328px; 280 | } 281 | 282 | 100% { 283 | stroke-dashoffset: 0; 284 | stroke-dasharray: 328px; 285 | } 286 | } 287 | 288 | .gmt-main-icon-8 { 289 | -webkit-animation: animate-svg-stroke-8 1s ease-in 0.84s both, 290 | animate-svg-fill-8 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1.5s both; 291 | animation: animate-svg-stroke-8 1s ease-in 0.84s both, 292 | animate-svg-fill-8 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1.5s both; 293 | } 294 | 295 | @-webkit-keyframes animate-svg-stroke-9 { 296 | 0% { 297 | stroke-dashoffset: 1005.3450317382812px; 298 | stroke-dasharray: 1005.3450317382812px; 299 | } 300 | 301 | 100% { 302 | stroke-dashoffset: 0; 303 | stroke-dasharray: 1005.3450317382812px; 304 | } 305 | } 306 | 307 | @keyframes animate-svg-stroke-9 { 308 | 0% { 309 | stroke-dashoffset: 1005.3450317382812px; 310 | stroke-dasharray: 1005.3450317382812px; 311 | } 312 | 313 | 100% { 314 | stroke-dashoffset: 0; 315 | stroke-dasharray: 1005.3450317382812px; 316 | } 317 | } 318 | 319 | @-webkit-keyframes animate-svg-fill-9 { 320 | 0% { 321 | fill: transparent; 322 | } 323 | 324 | 100% { 325 | fill: rgb(0, 0, 0); 326 | } 327 | } 328 | 329 | @keyframes animate-svg-fill-9 { 330 | 0% { 331 | fill: transparent; 332 | } 333 | 334 | 100% { 335 | fill: rgb(0, 0, 0); 336 | } 337 | } 338 | 339 | .gmt-main-icon-9 { 340 | -webkit-animation: animate-svg-stroke-9 1s ease-in 0.96s both, 341 | animate-svg-fill-9 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1.6s both; 342 | animation: animate-svg-stroke-9 1s ease-in 0.96s both, 343 | animate-svg-fill-9 0.75s cubic-bezier(0.23, 1, 0.32, 1) 1.6s both; 344 | } -------------------------------------------------------------------------------- /gmt/templates/writers/settings.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Settings{% endblock %} 3 | {% block body %} 4 | 16 |
17 |

18 | Profile Settings: 19 |

20 |

21 | Update your profile settings here, these will be used to writers display your profile on the site. 22 |

23 | {% if status %} 24 | 28 | {% endif %} 29 | 30 |
33 | 34 | 35 |
36 | 44 |
45 |
46 | 58 |
59 |
60 | 70 |
71 |
72 | 82 |
83 |
84 | 103 |
104 |
105 | 115 |
116 |
117 | 127 |
128 |
129 | 139 |
140 |
141 | 151 |
152 |
153 | 163 |
164 |
165 | 175 |
176 |
177 | 180 |
181 |
182 |
183 | {% endblock %} 184 | -------------------------------------------------------------------------------- /gmt/templates/writers/apply.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Apply as writer{% endblock %} 3 | {% block head %} 4 | 6 | 8 | 10 | 11 | 12 | 14 | 15 | 17 | 19 | 20 | 21 | {% endblock %} 22 | {% block body %} 23 |
24 |
25 |
26 |
27 | Step: 1 of 3 28 |
29 | Good Morning Tech Writer Application Form: 31 | 32 |
34 |
35 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | {% if status %} 44 | 48 | {% endif %} 49 |
53 | 54 |
55 | 64 | 73 | 81 |
82 | 84 | Proceed 85 | 86 |
87 |
88 | 121 | 173 |
174 |
175 |
176 | 177 |
178 | {% endblock %} 179 | -------------------------------------------------------------------------------- /gmt/templates/writers/register.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Register{% endblock %} 3 | {% block body %} 4 | 5 |
6 |
7 |
8 |
9 | Step: 1 of 3 10 |
11 | Creating your Profile: 13 | 14 |
16 |
17 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | {% if status %} 26 | 30 | {% endif %} 31 |
35 | 36 |
37 | 48 | 78 | 87 | 96 | 105 |
106 | 108 | Proceed 109 | 110 |
111 |
112 | 152 | 189 |
190 |
191 |
192 | 193 |
194 | {% endblock %} 195 | --------------------------------------------------------------------------------