Add basic HTTP API

This commit is contained in:
Ske 2018-08-02 00:36:50 +02:00
parent e831ef5921
commit 944f0093a9
13 changed files with 238 additions and 51 deletions

View File

@ -1,9 +1,10 @@
version: '3' version: '3'
services: services:
bot: bot:
build: build: src/
context: src/ entrypoint:
dockerfile: bot.Dockerfile - python
- bot_main.py
depends_on: depends_on:
- db - db
- influx - influx
@ -11,6 +12,16 @@ services:
- CLIENT_ID - CLIENT_ID
- TOKEN - TOKEN
restart: always restart: always
api:
build: src/
entrypoint:
- python
- api_main.py
depends_on:
- db
restart: always
ports:
- "2939:8080"
db: db:
image: postgres:alpine image: postgres:alpine
volumes: volumes:

View File

@ -7,5 +7,4 @@ ADD requirements.txt /app
RUN pip install --trusted-host pypi.python.org -r requirements.txt RUN pip install --trusted-host pypi.python.org -r requirements.txt
ADD . /app ADD . /app
ENTRYPOINT ["python", "bot_main.py"]

122
src/api_main.py Normal file
View File

@ -0,0 +1,122 @@
import logging
from aiohttp import web
from pluralkit import db, utils
logging.basicConfig(level=logging.INFO, format="[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s")
logger = logging.getLogger("pluralkit.api")
def db_handler(f):
async def inner(request):
async with request.app["pool"].acquire() as conn:
return await f(request, conn)
return inner
@db_handler
async def get_system(request: web.Request, conn):
system = await db.get_system_by_hid(conn, request.match_info["id"])
if not system:
raise web.HTTPNotFound()
members = await db.get_all_members(conn, system.id)
system_json = system.to_json()
system_json["members"] = [member.to_json() for member in members]
return web.json_response(system_json)
@db_handler
async def get_member(request: web.Request, conn):
member = await db.get_member_by_hid(conn, request.match_info["id"])
if not member:
raise web.HTTPNotFound()
return web.json_response(member.to_json())
@db_handler
async def get_switches(request: web.Request, conn):
system = await db.get_system_by_hid(conn, request.match_info["id"])
if not system:
raise web.HTTPNotFound()
switches = await utils.get_front_history(conn, system.id, 99999)
data = [{
"timestamp": stamp.isoformat(),
"members": [member.hid for member in members]
} for stamp, members in switches]
return web.json_response(data)
@db_handler
async def get_message(request: web.Request, conn):
message = await db.get_message(conn, request.match_info["id"])
if not message:
raise web.HTTPNotFound()
return web.json_response(message.to_json())
@db_handler
async def get_switch(request: web.Request, conn):
system = await db.get_system_by_hid(conn, request.match_info["id"])
if not system:
raise web.HTTPNotFound()
members, stamp = await utils.get_fronters(conn, system.id)
if not stamp:
# No switch has been registered at all
raise web.HTTPNotFound()
data = {
"timestamp": stamp.isoformat(),
"members": [member.to_json() for member in members]
}
return web.json_response(data)
@db_handler
async def get_switch_name(request: web.Request, conn):
system = await db.get_system_by_hid(conn, request.match_info["id"])
if not system:
raise web.HTTPNotFound()
members, stamp = await utils.get_fronters(conn, system.id)
return web.Response(text=members[0].name if members else "(nobody)")
@db_handler
async def get_switch_color(request: web.Request, conn):
system = await db.get_system_by_hid(conn, request.match_info["id"])
if not system:
raise web.HTTPNotFound()
members, stamp = await utils.get_fronters(conn, system.id)
return web.Response(text=members[0].color if members else "#ffffff")
app = web.Application()
app.add_routes([
web.get("/systems/{id}", get_system),
web.get("/systems/{id}/switches", get_switches),
web.get("/systems/{id}/switch", get_switch),
web.get("/systems/{id}/switch/name", get_switch_name),
web.get("/systems/{id}/switch/color", get_switch_color),
web.get("/members/{id}", get_member),
web.get("/messages/{id}", get_message)
])
async def run():
app["pool"] = await db.connect()
return app
web.run_app(run())

View File

@ -11,6 +11,15 @@ class System(namedtuple("System", ["id", "hid", "name", "description", "tag", "a
avatar_url: str avatar_url: str
created: datetime created: datetime
def to_json(self):
return {
"id": self.hid,
"name": self.name,
"description": self.description,
"tag": self.tag,
"avatar_url": self.avatar_url
}
class Member(namedtuple("Member", ["id", "hid", "system", "color", "avatar_url", "name", "birthday", "pronouns", "description", "prefix", "suffix", "created"])): class Member(namedtuple("Member", ["id", "hid", "system", "color", "avatar_url", "name", "birthday", "pronouns", "description", "prefix", "suffix", "created"])):
id: int id: int
hid: str hid: str
@ -23,4 +32,17 @@ class Member(namedtuple("Member", ["id", "hid", "system", "color", "avatar_url",
description: str description: str
prefix: str prefix: str
suffix: str suffix: str
created: datetime created: datetime
def to_json(self):
return {
"id": self.hid,
"name": self.name,
"color": self.color,
"avatar_url": self.avatar_url,
"birthday": self.birthday.isoformat() if self.birthday else None,
"pronouns": self.pronouns,
"description": self.description,
"prefix": self.prefix,
"suffix": self.suffix
}

View File

View File

@ -6,6 +6,7 @@ from typing import List
from discord.utils import oauth_url from discord.utils import oauth_url
import pluralkit.utils
from pluralkit.bot import utils from pluralkit.bot import utils
from pluralkit.bot.commands import * from pluralkit.bot.commands import *
@ -52,7 +53,7 @@ async def invite_link(ctx: CommandContext, args: List[str]):
async def export(ctx: CommandContext, args: List[str]): async def export(ctx: CommandContext, args: List[str]):
members = await db.get_all_members(ctx.conn, ctx.system.id) members = await db.get_all_members(ctx.conn, ctx.system.id)
accounts = await db.get_linked_accounts(ctx.conn, ctx.system.id) accounts = await db.get_linked_accounts(ctx.conn, ctx.system.id)
switches = await utils.get_front_history(ctx.conn, ctx.system.id, 999999) switches = await pluralkit.utils.get_front_history(ctx.conn, ctx.system.id, 999999)
system = ctx.system system = ctx.system
data = { data = {

View File

@ -5,6 +5,7 @@ from typing import List
import dateparser import dateparser
import humanize import humanize
import pluralkit.utils
from pluralkit import Member from pluralkit import Member
from pluralkit.bot import utils from pluralkit.bot import utils
from pluralkit.bot.commands import * from pluralkit.bot.commands import *
@ -27,7 +28,7 @@ async def switch_member(ctx: MemberCommandContext, args: List[str]):
# Compare requested switch IDs and existing fronter IDs to check for existing switches # Compare requested switch IDs and existing fronter IDs to check for existing switches
# Lists, because order matters, it makes sense to just swap fronters # Lists, because order matters, it makes sense to just swap fronters
member_ids = [member.id for member in members] member_ids = [member.id for member in members]
fronter_ids = (await utils.get_fronter_ids(ctx.conn, ctx.system.id))[0] fronter_ids = (await pluralkit.utils.get_fronter_ids(ctx.conn, ctx.system.id))[0]
if member_ids == fronter_ids: if member_ids == fronter_ids:
if len(members) == 1: if len(members) == 1:
raise CommandError("{} is already fronting.".format(members[0].name)) raise CommandError("{} is already fronting.".format(members[0].name))
@ -51,7 +52,7 @@ async def switch_member(ctx: MemberCommandContext, args: List[str]):
@command(cmd="switch out", description="Registers a switch with no one in front.", category="Switching commands") @command(cmd="switch out", description="Registers a switch with no one in front.", category="Switching commands")
async def switch_out(ctx: MemberCommandContext, args: List[str]): async def switch_out(ctx: MemberCommandContext, args: List[str]):
# Get current fronters # Get current fronters
fronters, _ = await utils.get_fronter_ids(ctx.conn, system_id=ctx.system.id) fronters, _ = await pluralkit.utils.get_fronter_ids(ctx.conn, system_id=ctx.system.id)
if not fronters: if not fronters:
raise CommandError("There's already no one in front.") raise CommandError("There's already no one in front.")
@ -79,7 +80,7 @@ async def switch_move(ctx: MemberCommandContext, args: List[str]):
# Make sure it all runs in a big transaction for atomicity # Make sure it all runs in a big transaction for atomicity
async with ctx.conn.transaction(): async with ctx.conn.transaction():
# Get the last two switches to make sure the switch to move isn't before the second-last switch # Get the last two switches to make sure the switch to move isn't before the second-last switch
last_two_switches = await utils.get_front_history(ctx.conn, ctx.system.id, count=2) last_two_switches = await pluralkit.utils.get_front_history(ctx.conn, ctx.system.id, count=2)
if len(last_two_switches) == 0: if len(last_two_switches) == 0:
raise CommandError("There are no registered switches for this system.") raise CommandError("There are no registered switches for this system.")

View File

@ -4,6 +4,7 @@ from urllib.parse import urlparse
import humanize import humanize
import pluralkit.utils
from pluralkit.bot import utils from pluralkit.bot import utils
from pluralkit.bot.commands import * from pluralkit.bot.commands import *
@ -153,7 +154,7 @@ async def system_fronter(ctx: CommandContext, args: List[str]):
if system is None: if system is None:
raise CommandError("Can't find system \"{}\".".format(args[0])) raise CommandError("Can't find system \"{}\".".format(args[0]))
fronters, timestamp = await utils.get_fronters(ctx.conn, system_id=system.id) fronters, timestamp = await pluralkit.utils.get_fronters(ctx.conn, system_id=system.id)
fronter_names = [member.name for member in fronters] fronter_names = [member.name for member in fronters]
embed = utils.make_default_embed(None) embed = utils.make_default_embed(None)
@ -182,7 +183,7 @@ async def system_fronthistory(ctx: CommandContext, args: List[str]):
raise CommandError("Can't find system \"{}\".".format(args[0])) raise CommandError("Can't find system \"{}\".".format(args[0]))
lines = [] lines = []
front_history = await utils.get_front_history(ctx.conn, system.id, count=10) front_history = await pluralkit.utils.get_front_history(ctx.conn, system.id, count=10)
for i, (timestamp, members) in enumerate(front_history): for i, (timestamp, members) in enumerate(front_history):
# Special case when no one's fronting # Special case when no one's fronting
if len(members) == 0: if len(members) == 0:

View File

@ -1,16 +1,13 @@
from datetime import datetime
import logging import logging
import random import random
import re import re
from typing import List, Tuple
import string import string
import asyncio
import asyncpg
import discord import discord
import humanize import humanize
from pluralkit import System, Member, db from pluralkit import System, Member, db
from pluralkit.utils import get_fronters
logger = logging.getLogger("pluralkit.utils") logger = logging.getLogger("pluralkit.utils")
@ -53,42 +50,6 @@ def parse_channel_mention(mention: str, server: discord.Server) -> discord.Chann
except ValueError: except ValueError:
return None return None
async def get_fronter_ids(conn, system_id) -> (List[int], datetime):
switches = await db.front_history(conn, system_id=system_id, count=1)
if not switches:
return [], None
if not switches[0]["members"]:
return [], switches[0]["timestamp"]
return switches[0]["members"], switches[0]["timestamp"]
async def get_fronters(conn, system_id) -> (List[Member], datetime):
member_ids, timestamp = await get_fronter_ids(conn, system_id)
# Collect in dict and then look up as list, to preserve return order
members = {member.id: member for member in await db.get_members(conn, member_ids)}
return [members[member_id] for member_id in member_ids], timestamp
async def get_front_history(conn, system_id, count) -> List[Tuple[datetime, List[Member]]]:
# Get history from DB
switches = await db.front_history(conn, system_id=system_id, count=count)
if not switches:
return []
# Get all unique IDs referenced
all_member_ids = {id for switch in switches for id in switch["members"]}
# And look them up in the database into a dict
all_members = {member.id: member for member in await db.get_members(conn, list(all_member_ids))}
# Collect in array and return
out = []
for switch in switches:
timestamp = switch["timestamp"]
members = [all_members[id] for id in switch["members"]]
out.append((timestamp, members))
return out
async def get_system_fuzzy(conn, client: discord.Client, key) -> System: async def get_system_fuzzy(conn, client: discord.Client, key) -> System:
if isinstance(key, discord.User): if isinstance(key, discord.User):

View File

@ -6,6 +6,7 @@ import time
import asyncpg import asyncpg
import asyncpg.exceptions import asyncpg.exceptions
from discord.utils import snowflake_time
from pluralkit import System, Member, stats from pluralkit import System, Member, stats
@ -210,6 +211,17 @@ class MessageInfo(namedtuple("MemberInfo", ["mid", "channel", "member", "content
system_name: str system_name: str
system_hid: str system_hid: str
def to_json(self):
return {
"id": str(self.mid),
"channel": str(self.channel),
"member": self.hid,
"system": self.system_hid,
"message_sender": str(self.sender),
"content": self.content,
"timestamp": snowflake_time(self.mid).isoformat()
}
@db_wrap @db_wrap
async def get_message_by_sender_and_id(conn, message_id: str, sender_id: str) -> MessageInfo: async def get_message_by_sender_and_id(conn, message_id: str, sender_id: str) -> MessageInfo:
row = await conn.fetchrow("""select row = await conn.fetchrow("""select

View File

@ -7,6 +7,9 @@ async def connect():
await client.create_database(db="pluralkit") await client.create_database(db="pluralkit")
async def report_db_query(query_name, time, success): async def report_db_query(query_name, time, success):
if not client:
return
await client.write({ await client.write({
"measurement": "database_query", "measurement": "database_query",
"tags": {"query": query_name}, "tags": {"query": query_name},
@ -14,6 +17,9 @@ async def report_db_query(query_name, time, success):
}) })
async def report_command(command_name, execution_time, response_time): async def report_command(command_name, execution_time, response_time):
if not client:
return
await client.write({ await client.write({
"measurement": "command", "measurement": "command",
"tags": {"command": command_name}, "tags": {"command": command_name},
@ -21,12 +27,18 @@ async def report_command(command_name, execution_time, response_time):
}) })
async def report_webhook(time, success): async def report_webhook(time, success):
if not client:
return
await client.write({ await client.write({
"measurement": "webhook", "measurement": "webhook",
"fields": {"response_time": time, "success": int(success)} "fields": {"response_time": time, "success": int(success)}
}) })
async def report_periodical_stats(conn): async def report_periodical_stats(conn):
if not client:
return
from pluralkit import db from pluralkit import db
systems = await db.system_count(conn) systems = await db.system_count(conn)

44
src/pluralkit/utils.py Normal file
View File

@ -0,0 +1,44 @@
from datetime import datetime
from typing import List, Tuple
from pluralkit import db, Member
async def get_fronter_ids(conn, system_id) -> (List[int], datetime):
switches = await db.front_history(conn, system_id=system_id, count=1)
if not switches:
return [], None
if not switches[0]["members"]:
return [], switches[0]["timestamp"]
return switches[0]["members"], switches[0]["timestamp"]
async def get_fronters(conn, system_id) -> (List[Member], datetime):
member_ids, timestamp = await get_fronter_ids(conn, system_id)
# Collect in dict and then look up as list, to preserve return order
members = {member.id: member for member in await db.get_members(conn, member_ids)}
return [members[member_id] for member_id in member_ids], timestamp
async def get_front_history(conn, system_id, count) -> List[Tuple[datetime, List[Member]]]:
# Get history from DB
switches = await db.front_history(conn, system_id=system_id, count=count)
if not switches:
return []
# Get all unique IDs referenced
all_member_ids = {id for switch in switches for id in switch["members"]}
# And look them up in the database into a dict
all_members = {member.id: member for member in await db.get_members(conn, list(all_member_ids))}
# Collect in array and return
out = []
for switch in switches:
timestamp = switch["timestamp"]
members = [all_members[id] for id in switch["members"]]
out.append((timestamp, members))
return out

View File

@ -1,3 +1,4 @@
aiodns
aiohttp aiohttp
aioinflux aioinflux
asyncpg asyncpg