This commit is contained in:
Benjamyn Love 2023-03-08 11:43:10 +11:00
parent bc0f4b6b37
commit 472dace156
18 changed files with 427 additions and 135 deletions

8
.gitignore vendored
View File

@ -1,8 +1,16 @@
# Configs
config/service-acct.json
config/db.py
# Logs
*.log
# JetBrains stuff
.idea/
# Python stuff
__pycache__/
venv/
# Secrets
*.secret

42
__init__.py Normal file
View File

@ -0,0 +1,42 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from .config import db as db_config
db = SQLAlchemy()
def create_app():
app = Flask(__name__)
app.config['SECRET_KEY'] = '//'
app.config["SQLALCHEMY_DATABASE_URI"] = db_config.SQL_URI
db.init_app(app)
login_manager = LoginManager()
login_manager.login_view = 'auth.login'
login_manager.init_app(app)
from .models import User
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
from . import models
with app.app_context():
print("Applying the DB models")
db.create_all()
# blueprint for auth routes in our app
from .auth import auth as auth_blueprint
app.register_blueprint(auth_blueprint)
# blueprint for non-auth parts of app
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
# blueprint for chatbot
from .chatbot import chatbot as chatbot_blueprint
app.register_blueprint(chatbot_blueprint)
return app

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

67
auth.py Normal file
View File

@ -0,0 +1,67 @@
from flask import Blueprint, render_template, redirect, url_for, request, flash
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import login_user, login_required, logout_user
from .models import User
from . import db
auth = Blueprint('auth', __name__)
@auth.route('/login')
def login():
return render_template('login.html')
@auth.route('/login', methods=['POST'])
def login_post():
email = request.form.get('email')
password = request.form.get('password')
remember = True if request.form.get('remember') else False
user = User.query.filter_by(email=email).first()
# Check if the user actually exists
# Take the supplied password and hash it, compare it to the hashed password
# If they match we gucci
if not user or not check_password_hash(user.password, password):
flash('Please check your login details and try again')
return redirect(url_for('auth.login'))
# If we get here we are gucci
login_user(user, remember=remember)
return redirect(url_for('main.profile'))
@auth.route('/signup')
def signup():
return render_template('signup.html')
@auth.route('/signup', methods=['POST'])
def signup_post():
email = request.form.get('email')
name = request.form.get('name')
password = request.form.get('password')
google_id = request.form.get('google_id')
user = User.query.filter_by(email=email).first()
if user:
flash('Email already exists for user')
return redirect(url_for('auth.signup'))
user = User.query.filter_by(google_id=google_id).first()
if user:
flash('Google ID already in use')
return redirect(url_for('auth.signup'))
new_user = User(email=email, name=name, password=generate_password_hash(password, method='sha256'), google_id=google_id)
db.session.add(new_user)
db.session.commit()
# Code to validate and add the user to the database
return redirect(url_for('auth.login'))
@auth.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('main.index'))

132
chatbot.py Normal file
View File

@ -0,0 +1,132 @@
# Copyright 2017 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# pylint: disable=invalid-name
"""
Hangouts Chat bot that responds to events and messages from a room asynchronously.
"""
# [START async-bot]
import logging
from os import environ
from flask import Blueprint, render_template, request, json
from google.oauth2 import service_account
from googleapiclient.discovery import build
logging.basicConfig(filename='example.log', level=logging.DEBUG)
chatbot = Blueprint('chatbot', __name__)
scopes = ['https://www.googleapis.com/auth/chat.bot']
credentials = service_account.Credentials.from_service_account_file('./config/service-acct.json')
# credentials, project_id = google.auth.default()
credentials = credentials.with_scopes(scopes=scopes)
chat = build('chat', 'v1', credentials=credentials)
@chatbot.route('/bot/', methods=['POST'])
def home_post():
"""Respond to POST requests to this endpoint.
All requests sent to this endpoint from Hangouts Chat are POST
requests.
"""
event_data = request.get_json()
logging.debug(event_data)
resp = None
# If the bot is removed from the space, it doesn't post a message
# to the space. Instead, log a message showing that the bot was removed.
if event_data['type'] == 'REMOVED_FROM_SPACE':
logging.info('Bot removed from %s', event_data['space']['name'])
return json.jsonify({})
resp = format_response(event_data)
space_name = event_data['space']['name']
send_async_response(resp, space_name)
# Return empty jsom response since message already sent via REST API
return json.jsonify({})
# [START async-response]
def send_async_response(response, space_name):
"""Sends a response back to the Hangouts Chat room asynchronously.
Args:
response: the response payload
space_name: The URL of the Hangouts Chat room
"""
chat.spaces().messages().create(
parent=space_name,
body=response).execute()
# [END async-response]
def format_response(event):
"""Determine what response to provide based upon event data.
Args:
event: A dictionary with the event data.
"""
event_type = event['type']
text = ""
sender_name = event['user']['displayName']
# Case 1: The bot was added to a room
if event_type == 'ADDED_TO_SPACE' and event['space']['type'] == 'ROOM':
text = 'Thanks for adding me to {}!'.format(event['space']['displayName'])
# Case 2: The bot was added to a DM
elif event_type == 'ADDED_TO_SPACE' and event['space']['type'] == 'DM':
text = 'Thanks for adding me to a DM, {}!'.format(sender_name)
elif event_type == 'MESSAGE':
text = 'Your message, {}: "{}"'.format(sender_name, event['message']['text'])
response = {'text': text}
# The following three lines of code update the thread that raised the event.
# Delete them if you want to send the message in a new thread.
if event_type == 'MESSAGE' and event['message']['thread'] is not None:
thread_id = event['message']['thread']
response['thread'] = thread_id
return response
# [END async-bot]
@chatbot.route('/bot/', methods=['GET'])
def home_get():
"""Respond to GET requests to this endpoint.
This function responds to requests with a simple HTML landing page for this
application Engine instance.
"""
return render_template('home.html')

0
config/__init__.py Normal file
View File

1
config/db.py.sample Normal file
View File

@ -0,0 +1 @@
SQL_URI = "sqlite:///database.db"

146
main.py
View File

@ -1,137 +1,15 @@
# Copyright 2017 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# pylint: disable=invalid-name
"""
Hangouts Chat bot that responds to events and messages from a room asynchronously.
"""
from flask import Blueprint, render_template
from flask_login import login_required, current_user
from . import db
# [START async-bot]
main = Blueprint('main', __name__)
@main.route('/')
def index():
return render_template('index.html')
import logging
from os import environ
from flask import Flask, render_template, request, json
from google.oauth2 import service_account
from googleapiclient.discovery import build
logging.basicConfig(filename='example.log', level=logging.DEBUG)
application = Flask(__name__)
logging.info(environ)
scopes = ['https://www.googleapis.com/auth/chat.bot']
credentials = service_account.Credentials.from_service_account_file('./config/service-acct.json')
# credentials, project_id = google.auth.default()
credentials = credentials.with_scopes(scopes=scopes)
chat = build('chat', 'v1', credentials=credentials)
@application.route('/', methods=['POST'])
def home_post():
"""Respond to POST requests to this endpoint.
All requests sent to this endpoint from Hangouts Chat are POST
requests.
"""
event_data = request.get_json()
logging.debug(event_data)
resp = None
# If the bot is removed from the space, it doesn't post a message
# to the space. Instead, log a message showing that the bot was removed.
if event_data['type'] == 'REMOVED_FROM_SPACE':
logging.info('Bot removed from %s', event_data['space']['name'])
return json.jsonify({})
resp = format_response(event_data)
space_name = event_data['space']['name']
send_async_response(resp, space_name)
# Return empty jsom response since message already sent via REST API
return json.jsonify({})
# [START async-response]
def send_async_response(response, space_name):
"""Sends a response back to the Hangouts Chat room asynchronously.
Args:
response: the response payload
space_name: The URL of the Hangouts Chat room
"""
chat.spaces().messages().create(
parent=space_name,
body=response).execute()
# [END async-response]
def format_response(event):
"""Determine what response to provide based upon event data.
Args:
event: A dictionary with the event data.
"""
event_type = event['type']
text = ""
sender_name = event['user']['displayName']
# Case 1: The bot was added to a room
if event_type == 'ADDED_TO_SPACE' and event['space']['type'] == 'ROOM':
text = 'Thanks for adding me to {}!'.format(event['space']['displayName'])
# Case 2: The bot was added to a DM
elif event_type == 'ADDED_TO_SPACE' and event['space']['type'] == 'DM':
text = 'Thanks for adding me to a DM, {}!'.format(sender_name)
elif event_type == 'MESSAGE':
text = 'Your message, {}: "{}"'.format(sender_name, event['message']['text'])
response = {'text': text}
# The following three lines of code update the thread that raised the event.
# Delete them if you want to send the message in a new thread.
if event_type == 'MESSAGE' and event['message']['thread'] is not None:
thread_id = event['message']['thread']
response['thread'] = thread_id
return response
# [END async-bot]
@application.route('/', methods=['GET'])
def home_get():
"""Respond to GET requests to this endpoint.
This function responds to requests with a simple HTML landing page for this
application Engine instance.
"""
return render_template('home.html')
if __name__ == '__main__':
# This is used when running locally. Gunicorn is used to run the
# application on Google application Engine. See entrypoint in application.yaml.
application.run(host='127.0.0.1', port=8080, debug=True)
@main.route('/profile')
@login_required
def profile():
return render_template('profile.html', name=current_user.name, google_id=current_user.google_id)

9
models.py Normal file
View File

@ -0,0 +1,9 @@
from flask_login import UserMixin
from . import db
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(100), unique=True)
password = db.Column(db.String(100))
name = db.Column(db.String(1000))
google_id = db.Column(db.String(30), unique=True)

View File

@ -2,3 +2,6 @@ Flask~=2.2.3
google-api-python-client
google-auth
google
mysqlclient
flask-login
flask_sqlalchemy

56
templates/base.html Normal file
View File

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Flask Auth Example</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css" />
</head>
<body>
<section class="hero is-primary is-fullheight">
<div class="hero-head">
<nav class="navbar">
<div class="container">
<div id="navbarMenuHeroA" class="navbar-menu">
<div class="navbar-end">
<a href="{{ url_for('main.index') }}" class="navbar-item">
Home
</a>
{% if current_user.is_authenticated %}
<a href="{{ url_for('main.profile') }}" class="navbar-item">
Profile
</a>
{% endif %}
{% if not current_user.is_authenticated %}
<a href="{{ url_for('auth.login') }}" class="navbar-item">
Login
</a>
<a href="{{ url_for('auth.signup') }}" class="navbar-item">
Sign Up
</a>
{% endif %}
{% if current_user.is_authenticated %}
<a href="{{ url_for('auth.logout') }}" class="navbar-item">
Logout
</a>
{% endif %}
</div>
</div>
</div>
</nav>
</div>
<div class="hero-body">
<div class="container has-text-centered">
{% block content %}
{% endblock %}
</div>
</div>
</section>
</body>
</html>

10
templates/index.html Normal file
View File

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block content %}
<h1 class="title">
Flask Login Example
</h1>
<h2 class="subtitle">
Easy authentication and authorization in Flask.
</h2>
{% endblock %}

36
templates/login.html Normal file
View File

@ -0,0 +1,36 @@
{% extends "base.html" %}
{% block content %}
<div class="column is-4 is-offset-4">
<h3 class="title">Login</h3>
<div class="box">
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="notification is-danger">
{{ messages[0] }}
</div>
{% endif %}
{% endwith %}
<form method="POST" action="/login">
<div class="field">
<div class="control">
<input class="input is-large" type="email" name="email" placeholder="Your Email" autofocus="">
</div>
</div>
<div class="field">
<div class="control">
<input class="input is-large" type="password" name="password" placeholder="Your Password">
</div>
</div>
<div class="field">
<label class="checkbox">
<input type="checkbox" name="remember">
Remember me
</label>
</div>
<button class="button is-block is-info is-large is-fullwidth">Login</button>
</form>
</div>
</div>
{% endblock %}

8
templates/profile.html Normal file
View File

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block content %}
<h1 class="title">
Welcome, {{ name }}! <p>
Your google ID is {{ google_id }}
</h1>
{% endblock %}

42
templates/signup.html Normal file
View File

@ -0,0 +1,42 @@
{% extends "base.html" %}
{% block content %}
<div class="column is-4 is-offset-4">
<h3 class="title">Sign Up</h3>
<div class="box">
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="notification is-danger">
{{ messages[0] }}. Go to <a href="{{ url_for('auth.login') }}">login page </a>.
</div>
{% endif %}
{% endwith %}
<form method="POST" action="/signup">
<div class="field">
<div class="control">
<input class="input is-large" type="email" name="email" placeholder="Email" autofocus="">
</div>
</div>
<div class="field">
<div class="control">
<input class="input is-large" type="text" name="name" placeholder="Name" autofocus="">
</div>
</div>
<div class="field">
<div class="control">
<input class="input is-large" type="password" name="password" placeholder="Password">
</div>
</div>
<div class="field">
<div class="control">
<input class="input is-large" type="text" name="google_id" placeholder="Google ID">
</div>
</div>
<button class="button is-block is-info is-large is-fullwidth">Sign Up</button>
</form>
</div>
</div>
{% endblock %}