Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
2fceb6679f | |||
06a9bbe343 |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
__pycache__
|
9
shell.nix
Normal file
9
shell.nix
Normal file
@ -0,0 +1,9 @@
|
||||
{ pkgs ? import <nixpkgs> {} }:
|
||||
pkgs.mkShell {
|
||||
# nativeBuildInputs is usually what you want -- tools you need to run
|
||||
nativeBuildInputs = with pkgs; [
|
||||
buildPackages.python311Packages.django
|
||||
buildPackages.python311Packages.requests
|
||||
buildPackages.python311Packages.qrcode
|
||||
];
|
||||
}
|
1
tfbbridge/.gitignore
vendored
Normal file
1
tfbbridge/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
db.sqlite3
|
1
tfbbridge/bridge/.gitignore
vendored
Normal file
1
tfbbridge/bridge/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
migrations
|
0
tfbbridge/bridge/__init__.py
Normal file
0
tfbbridge/bridge/__init__.py
Normal file
5
tfbbridge/bridge/admin.py
Normal file
5
tfbbridge/bridge/admin.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Organization
|
||||
|
||||
admin.site.register(Organization)
|
5
tfbbridge/bridge/apps.py
Normal file
5
tfbbridge/bridge/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
class BridgeConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'bridge'
|
9
tfbbridge/bridge/models.py
Normal file
9
tfbbridge/bridge/models.py
Normal file
@ -0,0 +1,9 @@
|
||||
from django.db import models
|
||||
|
||||
class Organization(models.Model):
|
||||
name = models.CharField(
|
||||
max_length=256,
|
||||
)
|
||||
owner = models.CharField(max_length=256)
|
||||
trusted_users = models.TextField() # either json or empty
|
||||
channel_info = models.TextField() # either json or empty
|
31
tfbbridge/bridge/templates/add_channel_to_organization.html
Normal file
31
tfbbridge/bridge/templates/add_channel_to_organization.html
Normal file
@ -0,0 +1,31 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block 'body' %}
|
||||
|
||||
<h1>Add groups to "{{ org.name }}"</h1>
|
||||
|
||||
{% if trusted %}
|
||||
<form class="form-horizontal" action="{% url 'add_trusted_channel' org.id %}" method="POST">
|
||||
{% else %}
|
||||
<form class="form-horizontal" action="{% url 'add_channel_to_organization' org.id %}" method="POST">
|
||||
{% endif %}
|
||||
<style>
|
||||
.name-modified {
|
||||
font-weight: 100;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% for group in groups %}
|
||||
<div class="checkbox-inline">
|
||||
<input type="checkbox" id="g_{{ group.id }}" value="{{ group.id }}" name="g_{{ group.id }}" />
|
||||
<label for="g_{{ group.id }}" class="name-modified">{{ group.name }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Create organization</button>
|
||||
</div>
|
||||
|
||||
{% csrf_token %}
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
39
tfbbridge/bridge/templates/add_user.html
Normal file
39
tfbbridge/bridge/templates/add_user.html
Normal file
@ -0,0 +1,39 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block 'body' %}
|
||||
<h1>Add a trusted user</h1>
|
||||
|
||||
<form class="form-horizontal" action="{% url 'add_trusted_user' org.id %}" method="POST">
|
||||
<div class="form-group-inline">
|
||||
<input type="search" id="searchbox" class="form-control" placeholder="Search users..."></input>
|
||||
</div>
|
||||
|
||||
{% for user in users %}
|
||||
<div class="checkbox-inline user_sortable">
|
||||
<input type="checkbox" id="u_{{ user.id }}" value="{{ user.id }}" name="u_{{ user.id }}" />
|
||||
<label for="u_{{ user.id }}" class="name-modified">{{ user.name }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<script>
|
||||
const usersearch = document.getElementById("searchbox");
|
||||
const userchecks = document.getElementsByClassName("user_sortable");
|
||||
usersearch.addEventListener("keyup", function () {
|
||||
var query = usersearch.value.toLowerCase();
|
||||
for (let user of userchecks) {
|
||||
var name = user.children[1].innerHTML.toLowerCase()
|
||||
if(name.includes(query)) {
|
||||
user.style.display = null;
|
||||
} else {
|
||||
user.style.display = "none";
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Add user</button>
|
||||
</div>
|
||||
|
||||
{% csrf_token %}
|
||||
</form>
|
||||
{% endblock %}
|
41
tfbbridge/bridge/templates/base.html
Normal file
41
tfbbridge/bridge/templates/base.html
Normal file
@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
{% if title %}
|
||||
<title>{{ title }} - tfb-groupme-bridge</title>
|
||||
{% else %}
|
||||
<title>tfb-groupme-bridge</title>
|
||||
{% endif %}
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-inverse">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="/groupme">tfb-groupme-bridge</a>
|
||||
</div>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
{% if request.session.logged_in %}
|
||||
<li class="dropdown">
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">{{ request.session.groupme_name }} <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'logout' %}">Log out</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% else %}
|
||||
<li><a href="{% url 'login' %}"><span class="glyphicon glyphicon-log-in"></span> Login</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container">
|
||||
{% block 'body' %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
74
tfbbridge/bridge/templates/create_organization.html
Normal file
74
tfbbridge/bridge/templates/create_organization.html
Normal file
@ -0,0 +1,74 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block 'body' %}
|
||||
<h1>Create a new organization</h1>
|
||||
|
||||
<p><i>You will be the administrator of this organization. If you don't want to be, <b>do not create the organization under your account — use someone else's!</b> You <b>won't be able to transfer ownership later</b>, so make a wise decision now.</i></p>
|
||||
<form class="form-horizontal" action="{% url 'add_organization_flow_select' %}" method="POST">
|
||||
<div class="form-group-inline">
|
||||
<label class="control-label" for="organization-name">Organization name:</label>
|
||||
<input type="text" id="organization-name" name="name" class="form-control">
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.name-modified {
|
||||
font-weight: 100;
|
||||
}
|
||||
</style>
|
||||
|
||||
<h2>Choose trusted channels</h2>
|
||||
<p>
|
||||
In order to have a channel appear as a choice here, you must be in that channel and you must be an admin in that channel.
|
||||
Trusted users can only send messages from trusted channels.
|
||||
|
||||
(For technical reasons, if you're in more than 200 groups, we don't show all of them.
|
||||
Also, you're in <i>200 groups?</i>)
|
||||
</p>
|
||||
|
||||
{% for group in groups %}
|
||||
<div class="checkbox-inline">
|
||||
<input type="checkbox" id="g_{{ group.id }}" value="{{ group.id }}" name="g_{{ group.id }}" />
|
||||
<label for="g_{{ group.id }}" class="name-modified">{{ group.name }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<h2>Choose trusted users</h2>
|
||||
<p>
|
||||
A user appears here if they're in one of the channels you administrate.
|
||||
If someone is a trusted user, they can send messages to every group in a trusted channel.
|
||||
Don't forget to add yourself!
|
||||
</p>
|
||||
|
||||
<div class="form-group-inline">
|
||||
<input type="search" id="searchbox" class="form-control" placeholder="Search users..."></input>
|
||||
</div>
|
||||
|
||||
{% for user in users %}
|
||||
<div class="checkbox-inline user_sortable">
|
||||
<input type="checkbox" id="u_{{ user.id }}" value="{{ user.id }}" name="u_{{ user.id }}" />
|
||||
<label for="u_{{ user.id }}" class="name-modified">{{ user.name }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<script>
|
||||
const usersearch = document.getElementById("searchbox");
|
||||
const userchecks = document.getElementsByClassName("user_sortable");
|
||||
usersearch.addEventListener("keyup", function () {
|
||||
var query = usersearch.value.toLowerCase();
|
||||
for (let user of userchecks) {
|
||||
var name = user.children[1].innerHTML.toLowerCase()
|
||||
if(name.includes(query)) {
|
||||
user.style.display = null;
|
||||
} else {
|
||||
user.style.display = "none";
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Create organization</button>
|
||||
</div>
|
||||
|
||||
{% csrf_token %}
|
||||
</form>
|
||||
{% endblock %}
|
8
tfbbridge/bridge/templates/group_add_succeed.html
Normal file
8
tfbbridge/bridge/templates/group_add_succeed.html
Normal file
@ -0,0 +1,8 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block 'body' %}
|
||||
|
||||
<h1>Group added</h1>
|
||||
|
||||
<p>Thanks!</p>
|
||||
|
||||
{% endblock %}
|
27
tfbbridge/bridge/templates/index_auth.html
Normal file
27
tfbbridge/bridge/templates/index_auth.html
Normal file
@ -0,0 +1,27 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block 'body' %}
|
||||
|
||||
<h1>Welcome, {{ request.session.groupme_name }}</h1>
|
||||
|
||||
<h2>Available actions</h2>
|
||||
|
||||
<ul>
|
||||
<li><a href="{% url 'add_organization_flow_select' %}">Create a new organization</a></li>
|
||||
<li><a href="{% url 'refresh_group_data' %}">Refresh group data</a></li>
|
||||
<li><a href="{% url 'logout' %}">Log out</a></li>
|
||||
</ul>
|
||||
|
||||
{% if organizations|length > 0 %}
|
||||
<h2>Your organizations</h2>
|
||||
<ul>
|
||||
{% for org in organizations %}
|
||||
<li><a href="{% url 'view_organization' org.id %}">{{ org.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<h2>Your organizations</h2>
|
||||
|
||||
<p>You don't seem to have any organizations — maybe <a href="{% url 'add_organization_flow_select' %}">create one</a>?</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
6
tfbbridge/bridge/templates/index_unauth.html
Normal file
6
tfbbridge/bridge/templates/index_unauth.html
Normal file
@ -0,0 +1,6 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block 'body' %}
|
||||
|
||||
<h1><code>tfb-groupme-bridge</code></h1>
|
||||
|
||||
{% endblock %}
|
12
tfbbridge/bridge/templates/logged_out.html
Normal file
12
tfbbridge/bridge/templates/logged_out.html
Normal file
@ -0,0 +1,12 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block 'body' %}
|
||||
|
||||
<h1>Logged out</h1>
|
||||
|
||||
You've been logged out.
|
||||
|
||||
<ul>
|
||||
<li><a href="{% url 'index' %}">Return to the homepage</a></li>
|
||||
</ul>
|
||||
|
||||
{% endblock %}
|
79
tfbbridge/bridge/templates/view_organization.html
Normal file
79
tfbbridge/bridge/templates/view_organization.html
Normal file
@ -0,0 +1,79 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block 'body' %}
|
||||
|
||||
<h1>{{ org.name }}</h1>
|
||||
|
||||
<h2>Channels</h2>
|
||||
<table class="table table-hover table-responseive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Level</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for channel in channels %}
|
||||
<tr>
|
||||
<td>{{ channel.name }}</td>
|
||||
<td>{{ channel.role }}</td>
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'delete_channel' org.id channel.gid %}" class="btn btn-danger"><span class="glyphicon glyphicon-trash"></span></a>
|
||||
{% if channel.role == "trusted" %}
|
||||
<a href="{% url 'demote_channel' org.id channel.gid %}" class="btn btn-primary"><span class="glyphicon glyphicon-arrow-down"></span></a>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-primary disabled"><span class="glyphicon glyphicon-arrow-down"></span></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'add_trusted_channel' org.id %}" class="btn btn-primary">Add new trusted channel</a>
|
||||
<a href="{{ scheme }}://{{ hostname }}{% url 'add_channel_to_organization' org.id %}" class="btn btn-primary">Add new follower channel</a>
|
||||
<button id="sharebutton" type="button" class="btn btn-primary">Copy share link</button>
|
||||
<a href="{% url 'qr_code' org.id %}" class="btn btn-primary">View QR Code</a>
|
||||
</div>
|
||||
|
||||
<h2>Trusted Users</h2>
|
||||
|
||||
<table class="table table-hover table-responseive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Sent messages</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>{{ user.name }}</td>
|
||||
<td>{{ user.number_messages }}</td>
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'remove_user' org.id user.uid %}" class="btn btn-danger"><span class="glyphicon glyphicon-trash"></span></a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'add_trusted_user' org.id %}" class="btn btn-primary">Add new trusted user</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const sharelink = document.getElementById("sharelink");
|
||||
const sharebutton = document.getElementById("sharebutton");
|
||||
document.getElementById("sharebutton").addEventListener("click", () => {
|
||||
navigator.clipboard.writeText(sharelink.href);
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
3
tfbbridge/bridge/tests.py
Normal file
3
tfbbridge/bridge/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
19
tfbbridge/bridge/urls.py
Normal file
19
tfbbridge/bridge/urls.py
Normal file
@ -0,0 +1,19 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.index, name="index"),
|
||||
path("logout", views.logout, name="logout"),
|
||||
path("login", views.login, name="login"),
|
||||
path("oauth_callback", views.handle_oauth, name="handle_oauth"),
|
||||
path("refresh_group_data", views.refresh_group_data, name="refresh_group_data"),
|
||||
path("add/organization", views.add_organization_flow_select, name="add_organization_flow_select"),
|
||||
path("organization/<int:org_id>", views.view_organization, name="view_organization"),
|
||||
path("organization/<int:org_id>/add_channel", views.add_channel_to_organization, name="add_channel_to_organization"),
|
||||
path("organization/<int:org_id>/add_trusted_channel", views.add_trusted_channel, name="add_trusted_channel"),
|
||||
path("organization/<int:org_id>/add_trusted_user", views.add_trusted_user, name="add_trusted_user"),
|
||||
path("organization/<int:org_id>/qr_code", views.qr_code, name="qr_code"),
|
||||
path("organization/<int:org_id>/delete_channel/<str:chan>", views.delete_channel, name="delete_channel"),
|
||||
path("organization/<int:org_id>/demote_channel/<str:chan>", views.demote_channel, name="demote_channel"),
|
||||
path("organization/<int:org_id>/remove_user/<str:user>", views.remove_user, name="remove_user"),
|
||||
]
|
443
tfbbridge/bridge/views.py
Normal file
443
tfbbridge/bridge/views.py
Normal file
@ -0,0 +1,443 @@
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
from .models import Organization
|
||||
import requests
|
||||
import pickle
|
||||
import qrcode
|
||||
import json
|
||||
import random
|
||||
|
||||
bot_name = "tfbbot"
|
||||
auth_link = "https://oauth.groupme.com/oauth/authorize?client_id=KbStJZciPGivvLuTN9XX6aXf6NHVcxDSiVR0wRC0cqVDuDfG"
|
||||
|
||||
def login(request):
|
||||
return redirect(auth_link)
|
||||
|
||||
def add_organization_flow_select(request):
|
||||
user_group_data = pickle.loads(
|
||||
bytes.fromhex(
|
||||
request.session["groupme_groups_pickle"]
|
||||
))
|
||||
user_group_data = user_group_data["response"]
|
||||
if request.method == "GET":
|
||||
rendered_groups = []
|
||||
rendered_users = []
|
||||
for group in user_group_data:
|
||||
our_id = request.session["groupme_id"]
|
||||
for user in group["members"]: # check if we're an admin
|
||||
if user["user_id"] == our_id:
|
||||
if "admin" in user["roles"]: # we are an admin
|
||||
rendered_groups.append({
|
||||
"name": group["name"],
|
||||
"id": group["group_id"]
|
||||
})
|
||||
|
||||
for user in group["members"]:
|
||||
rendered_users.append({
|
||||
"name": user["name"],
|
||||
"id": user["user_id"]
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
# remove duplicate users
|
||||
# https://stackoverflow.com/questions/9427163/remove-duplicate-dict-in-list-in-python
|
||||
rendered_users = [i for n, i in enumerate(rendered_users) if i not in rendered_users[n + 1:]]
|
||||
|
||||
# sort users by name
|
||||
rendered_users = sorted(rendered_users, key=lambda x: x["name"].lower())
|
||||
|
||||
context = {
|
||||
"request": request,
|
||||
"title": "Add organization",
|
||||
"groups": rendered_groups,
|
||||
"users": rendered_users
|
||||
}
|
||||
|
||||
return render(request, "create_organization.html", context)
|
||||
|
||||
elif request.method == "POST":
|
||||
name = request.POST["name"]
|
||||
users = [i[2:] for i in request.POST.keys() if i.startswith("u_")]
|
||||
groups = [i[2:] for i in request.POST.keys() if i.startswith("g_")]
|
||||
group_name_mapping = pickle.loads(
|
||||
bytes.fromhex(
|
||||
request.session["group_name_mapping"]
|
||||
))
|
||||
|
||||
org = Organization()
|
||||
org.owner = request.session["groupme_id"]
|
||||
org.name = name
|
||||
org.trusted_users = ""
|
||||
org.channel_info = ""
|
||||
org.save()
|
||||
|
||||
group_data_collected = []
|
||||
for group in groups:
|
||||
group_data = { "gid": group }
|
||||
data = {
|
||||
"bot": {
|
||||
"name": bot_name,
|
||||
"group_id": group,
|
||||
"active": True,
|
||||
"callback_url": "https://marching.beepboop.systems/groupme/organization/{}/callback/{}".format(
|
||||
org.id,
|
||||
str(random.randint(1, 100000))
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
r = requests.post(
|
||||
"https://api.groupme.com/v3/bots?token={}".format(
|
||||
request.session["groupme_access_token"]
|
||||
),
|
||||
data=json.dumps(data),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
data = r.json()
|
||||
print(r.url)
|
||||
group_data["bot_id"] = data["response"]["bot"]["bot_id"]
|
||||
group_data["name"] = group_name_mapping[group_data["gid"]]
|
||||
group_data["role"] = "trusted"
|
||||
|
||||
group_data_collected.append(group_data)
|
||||
|
||||
user_data_collected = []
|
||||
user_name_mappings = pickle.loads(bytes.fromhex(request.session["user_name_mapping"]))
|
||||
for user in users:
|
||||
try:
|
||||
username = user_name_mappings[user]
|
||||
except KeyError:
|
||||
username = "unknown"
|
||||
user_data = { "uid": user, "role": "trusted", "name": username, "number_messages": 0 }
|
||||
user_data_collected.append(user_data)
|
||||
|
||||
org.trusted_users = pickle.dumps(user_data_collected).hex()
|
||||
org.channel_info = pickle.dumps(group_data_collected).hex()
|
||||
org.save()
|
||||
|
||||
return redirect(reverse("view_organization", kwargs={"org_id": org.id}))
|
||||
|
||||
def handle_oauth(request):
|
||||
request.session["groupme_access_token"] = request.GET.get('access_token')
|
||||
|
||||
r = requests.get("https://api.groupme.com/v3/users/me?token={}".format(
|
||||
request.session["groupme_access_token"]
|
||||
))
|
||||
|
||||
data = r.json()
|
||||
request.session["groupme_id"] = data["response"]["id"]
|
||||
request.session["groupme_name"] = data["response"]["name"]
|
||||
|
||||
r = requests.get("https://api.groupme.com/v3/groups?token={}&per_page=200".format(
|
||||
request.session["groupme_access_token"]
|
||||
))
|
||||
|
||||
request.session["groupme_groups_pickle"] = pickle.dumps(r.json()).hex()
|
||||
request.session["logged_in"] = "yes"
|
||||
|
||||
group_name_mapping = {}
|
||||
for group in r.json()["response"]:
|
||||
group_name_mapping[group["id"]] = group["name"]
|
||||
request.session["group_name_mapping"] = pickle.dumps(group_name_mapping).hex()
|
||||
|
||||
user_name_mapping = {}
|
||||
for group in r.json()["response"]:
|
||||
for user in group["members"]:
|
||||
user_name_mapping[user["id"]] = user["name"]
|
||||
user_name_mapping[data["response"]["id"]] = data["response"]["name"]
|
||||
request.session["user_name_mapping"] = pickle.dumps(user_name_mapping).hex()
|
||||
|
||||
try:
|
||||
if request.session["needs_redirect"]:
|
||||
response = redirect(reverse(request.session["needs_redirect"]))
|
||||
del request.session["needs_redirect"]
|
||||
return response
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return redirect(reverse("index"))
|
||||
|
||||
def index(request):
|
||||
if "logged_in" in request.session:
|
||||
orgs = Organization.objects.filter(owner=request.session["groupme_id"])
|
||||
return render(request, "index_auth.html", {
|
||||
"title": "Home",
|
||||
"request": request,
|
||||
"organizations": orgs,
|
||||
})
|
||||
else:
|
||||
return render(request, "index_unauth.html", {
|
||||
"title": "Home",
|
||||
})
|
||||
|
||||
def refresh_group_data(request):
|
||||
return redirect(reverse("handle_oauth") + "?access_token={}".format(
|
||||
request.session["groupme_access_token"]
|
||||
))
|
||||
|
||||
def logout(request):
|
||||
if "logged_in" in request.session:
|
||||
request.session.flush()
|
||||
return render(request, "logged_out.html")
|
||||
else:
|
||||
return redirect(index)
|
||||
|
||||
def view_organization(request, org_id):
|
||||
org = Organization.objects.filter(id__exact=org_id)[0]
|
||||
scheme = 'https' if request.is_secure() else 'http'
|
||||
hostname = request.get_host()
|
||||
|
||||
if request.session["groupme_id"] == org.owner:
|
||||
org_channel_info = pickle.loads(bytes.fromhex(org.channel_info))
|
||||
org_trusted_users = pickle.loads(bytes.fromhex(org.trusted_users))
|
||||
return render(request, "view_organization.html", {
|
||||
"request": request,
|
||||
"org": org,
|
||||
"hostname": hostname,
|
||||
"scheme": scheme,
|
||||
"channels": org_channel_info,
|
||||
"users": org_trusted_users,
|
||||
})
|
||||
else:
|
||||
pass # blow up
|
||||
|
||||
def qr_code(request, org_id):
|
||||
org = Organization.objects.filter(id__exact=org_id)[0]
|
||||
scheme = 'https' if request.is_secure() else 'http'
|
||||
hostname = request.get_host()
|
||||
|
||||
qr = qrcode.QRCode(
|
||||
version=1,
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||||
box_size=10,
|
||||
border=4,
|
||||
)
|
||||
qr.add_data("{}://{}{}".format(
|
||||
scheme,
|
||||
hostname,
|
||||
reverse('add_channel_to_organization', args=[org.id]),
|
||||
))
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image(fill_color="black", back_color="white")
|
||||
|
||||
resp = HttpResponse(
|
||||
headers={
|
||||
"Content-Type": "image/png",
|
||||
}
|
||||
)
|
||||
img.save(resp)
|
||||
|
||||
return resp
|
||||
|
||||
def add_channel_to_organization(request, org_id, trusted=False):
|
||||
if not request.session["groupme_access_token"]:
|
||||
request.session["needs_redirect"] = "add_channel_to_organization"
|
||||
return redirect(reverse("login"))
|
||||
|
||||
org = Organization.objects.filter(id__exact=org_id)[0]
|
||||
|
||||
if request.method == "GET":
|
||||
user_group_data = pickle.loads(
|
||||
bytes.fromhex(
|
||||
request.session["groupme_groups_pickle"]
|
||||
))["response"]
|
||||
our_id = request.session["groupme_id"]
|
||||
groups_we_admin = []
|
||||
|
||||
for group in user_group_data:
|
||||
for user in group["members"]: # check if we're an admin
|
||||
if user["user_id"] == our_id:
|
||||
if "admin" in user["roles"]: # we are an admin
|
||||
groups_we_admin.append(group)
|
||||
|
||||
return render(request, "add_channel_to_organization.html", {
|
||||
"groups": groups_we_admin,
|
||||
"org": org,
|
||||
"request": request,
|
||||
"trusted": trusted,
|
||||
})
|
||||
|
||||
elif request.method == "POST":
|
||||
group_name_mapping = pickle.loads(
|
||||
bytes.fromhex(
|
||||
request.session["group_name_mapping"]
|
||||
))
|
||||
org = Organization.objects.filter(id__exact=org_id)[0]
|
||||
groups = [i[2:] for i in request.POST.keys() if i.startswith("g_")]
|
||||
org_group_info = pickle.loads(bytes.fromhex(org.channel_info))
|
||||
|
||||
if not trusted:
|
||||
for group in org_group_info:
|
||||
if group["gid"] in groups and group["role"] != "trusted":
|
||||
# prune the groups
|
||||
groups.remove(group["gid"])
|
||||
else:
|
||||
for index, group in enumerate(org_group_info):
|
||||
if group["gid"] in groups:
|
||||
org_group_info[index]["role"] = "trusted"
|
||||
|
||||
to_add = []
|
||||
# these are the unique groups
|
||||
for group in groups:
|
||||
if not trusted:
|
||||
data = {
|
||||
"bot": {
|
||||
"name": bot_name,
|
||||
"group_id": group,
|
||||
"active": True,
|
||||
}
|
||||
}
|
||||
else:
|
||||
data = {
|
||||
"bot": {
|
||||
"name": bot_name,
|
||||
"group_id": group,
|
||||
"active": True,
|
||||
"callback_url": "https://marching.beepboop.systems/groupme/organization/{}/callback/{}".format(
|
||||
org.id,
|
||||
str(random.randint(1, 100000))
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
r = requests.post(
|
||||
"https://api.groupme.com/v3/bots?token={}".format(
|
||||
request.session["groupme_access_token"]
|
||||
),
|
||||
data=json.dumps(data),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
print(r)
|
||||
data = r.json()
|
||||
|
||||
if not trusted:
|
||||
to_add.append({
|
||||
"gid": group,
|
||||
"bot_id": data["response"]["bot"]["bot_id"],
|
||||
"name": group_name_mapping[group],
|
||||
"role": "following",
|
||||
})
|
||||
else:
|
||||
to_add.append({
|
||||
"gid": group,
|
||||
"bot_id": data["response"]["bot"]["bot_id"],
|
||||
"name": group_name_mapping[group],
|
||||
"role": "trusted",
|
||||
})
|
||||
|
||||
org_group_info += to_add
|
||||
org.channel_info = pickle.dumps(org_group_info).hex()
|
||||
org.save()
|
||||
|
||||
if request.session["groupme_id"] == org.owner:
|
||||
return redirect(reverse('view_organization', args=[org.id]))
|
||||
else:
|
||||
return render(request, "group_add_succeed.html", {
|
||||
"request": request
|
||||
})
|
||||
|
||||
def add_trusted_channel(request, org_id):
|
||||
return add_channel_to_organization(request, org_id, trusted=True)
|
||||
|
||||
def delete_channel(request, org_id, chan):
|
||||
org = Organization.objects.filter(id__exact=org_id)[0]
|
||||
org_group_info = pickle.loads(bytes.fromhex(org.channel_info))
|
||||
|
||||
print(org_group_info)
|
||||
|
||||
for index, channel in enumerate(org_group_info):
|
||||
print(index, channel)
|
||||
if channel["gid"] == chan:
|
||||
org_group_info.pop(index)
|
||||
break
|
||||
|
||||
org.channel_info = pickle.dumps(org_group_info).hex()
|
||||
org.save()
|
||||
|
||||
return redirect(reverse('view_organization', args=[org.id]))
|
||||
|
||||
def demote_channel(request, org_id, chan):
|
||||
org = Organization.objects.filter(id__exact=org_id)[0]
|
||||
org_group_info = pickle.loads(bytes.fromhex(org.channel_info))
|
||||
|
||||
for index, channel in enumerate(org_group_info):
|
||||
print(index, channel)
|
||||
if channel["gid"] == chan:
|
||||
org_group_info[index]["role"] = "following"
|
||||
break
|
||||
|
||||
org.channel_info = pickle.dumps(org_group_info).hex()
|
||||
org.save()
|
||||
|
||||
return redirect(reverse('view_organization', args=[org.id]))
|
||||
|
||||
def add_trusted_user(request, org_id):
|
||||
org = Organization.objects.filter(id__exact=org_id)[0]
|
||||
org_trusted_users = pickle.loads(bytes.fromhex(org.trusted_users))
|
||||
user_group_data = pickle.loads(
|
||||
bytes.fromhex(
|
||||
request.session["groupme_groups_pickle"]
|
||||
))
|
||||
user_group_data = user_group_data["response"]
|
||||
|
||||
rendered_users = []
|
||||
for group in user_group_data:
|
||||
our_id = request.session["groupme_id"]
|
||||
for user in group["members"]: # check if we're an admin
|
||||
if user["user_id"] == our_id:
|
||||
if "admin" in user["roles"]: # we are an admin
|
||||
for user in group["members"]:
|
||||
rendered_users.append({
|
||||
"name": user["name"],
|
||||
"id": user["user_id"]
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
# remove duplicate users
|
||||
# https://stackoverflow.com/questions/9427163/remove-duplicate-dict-in-list-in-python
|
||||
rendered_users = [i for n, i in enumerate(rendered_users) if i not in rendered_users[n + 1:]]
|
||||
|
||||
if request.method == "GET":
|
||||
return render(request, "add_user.html", {
|
||||
"request": request,
|
||||
"users": rendered_users,
|
||||
"org": org,
|
||||
})
|
||||
|
||||
elif request.method == "POST":
|
||||
users = [i[2:] for i in request.POST.keys() if i.startswith("u_")]
|
||||
|
||||
user_name_mappings = pickle.loads(bytes.fromhex(request.session["user_name_mapping"]))
|
||||
for user in users:
|
||||
try:
|
||||
username = user_name_mappings[user]
|
||||
except KeyError:
|
||||
username = "unknown"
|
||||
user_data = { "uid": user, "role": "trusted", "name": username, "number_messages": 0 }
|
||||
org_trusted_users.append(user_data)
|
||||
|
||||
org.trusted_users = pickle.dumps(org_trusted_users).hex()
|
||||
org.save()
|
||||
|
||||
return redirect(reverse('view_organization', args=[org.id]))
|
||||
|
||||
def remove_user(request, org_id, user):
|
||||
org = Organization.objects.filter(id__exact=org_id)[0]
|
||||
org_trusted_users = pickle.loads(bytes.fromhex(org.trusted_users))
|
||||
|
||||
for index, tuser in enumerate(org_trusted_users):
|
||||
print(index, tuser)
|
||||
if tuser["uid"] == user:
|
||||
|
||||
org_trusted_users.pop(index)
|
||||
break
|
||||
|
||||
org.trusted_users = pickle.dumps(org_trusted_users).hex()
|
||||
org.save()
|
||||
|
||||
return redirect(reverse('view_organization', args=[org.id]))
|
21
tfbbridge/manage.py
Normal file
21
tfbbridge/manage.py
Normal file
@ -0,0 +1,21 @@
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tfb.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
0
tfbbridge/tfb/__init__.py
Normal file
0
tfbbridge/tfb/__init__.py
Normal file
16
tfbbridge/tfb/asgi.py
Normal file
16
tfbbridge/tfb/asgi.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for tfb project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tfb.settings')
|
||||
|
||||
application = get_asgi_application()
|
125
tfbbridge/tfb/settings.py
Normal file
125
tfbbridge/tfb/settings.py
Normal file
@ -0,0 +1,125 @@
|
||||
"""
|
||||
Django settings for tfb project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 4.2.16.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/4.2/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-%4qs)a3%&$ykwuu5d$0%^=8u1)b=t=t+vgo!rxvrq1z9+7)w1z'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ["127.0.0.1", "localhost"]
|
||||
CSRF_TRUSTED_ORIGINS = ["http://127.0.0.1:8000"]
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"bridge.apps.BridgeConfig",
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'tfb.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'tfb.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||
|
||||
STATIC_URL = 'groupme/static/'
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
7
tfbbridge/tfb/urls.py
Normal file
7
tfbbridge/tfb/urls.py
Normal file
@ -0,0 +1,7 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
path('groupme/', include('bridge.urls')),
|
||||
path('groupme/admin/', admin.site.urls),
|
||||
]
|
16
tfbbridge/tfb/wsgi.py
Normal file
16
tfbbridge/tfb/wsgi.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for tfb project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tfb.settings')
|
||||
|
||||
application = get_wsgi_application()
|
Loading…
Reference in New Issue
Block a user