WIP: Finish primitive functionality #1
|
@ -0,0 +1 @@
|
||||||
|
__pycache__
|
|
@ -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
|
||||||
|
];
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
db.sqlite3
|
|
@ -0,0 +1 @@
|
||||||
|
migrations
|
|
@ -0,0 +1,5 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import Organization
|
||||||
|
|
||||||
|
admin.site.register(Organization)
|
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
class BridgeConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'bridge'
|
|
@ -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
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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 %}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block 'body' %}
|
||||||
|
|
||||||
|
<h1>Group added</h1>
|
||||||
|
|
||||||
|
<p>Thanks!</p>
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -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 %}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block 'body' %}
|
||||||
|
|
||||||
|
<h1><code>tfb-groupme-bridge</code></h1>
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
|
@ -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"),
|
||||||
|
]
|
|
@ -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]))
|
|
@ -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,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()
|
|
@ -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'
|
|
@ -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),
|
||||||
|
]
|
|
@ -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