summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2022-12-14 17:19:10 +0100
committerTor Andersson <tor@ccxvii.net>2023-02-01 21:13:55 +0100
commit0dcf5df6d0197635ee3a6c837c21022e630f657f (patch)
tree6c3fc02c80e1c07555fd1394234a4107e0772884
parent2ebdd020dd83d85b5437189ed12595f6ae781262 (diff)
downloadserver-0dcf5df6d0197635ee3a6c837c21022e630f657f.tar.gz
Add Webhook notifications.
-rw-r--r--schema.sql8
-rw-r--r--server.js157
-rw-r--r--views/profile.pug11
-rw-r--r--views/webhook.pug46
4 files changed, 207 insertions, 15 deletions
diff --git a/schema.sql b/schema.sql
index d4a6864..e2ca8a4 100644
--- a/schema.sql
+++ b/schema.sql
@@ -54,6 +54,13 @@ create table if not exists last_notified (
primary key (game_id, user_id)
) without rowid;
+create table if not exists webhooks (
+ user_id integer primary key,
+ url text,
+ prefix text,
+ error text
+);
+
drop view if exists user_view;
create view user_view as
select
@@ -427,6 +434,7 @@ create trigger trigger_delete_on_users after delete on users
begin
delete from logins where user_id = old.user_id;
delete from tokens where user_id = old.user_id;
+ delete from webhooks where user_id = old.user_id;
delete from user_last_seen where user_id = old.user_id;
delete from last_notified where user_id = old.user_id;
delete from read_threads where user_id = old.user_id;
diff --git a/server.js b/server.js
index 1ca151b..4e6a487 100644
--- a/server.js
+++ b/server.js
@@ -3,6 +3,7 @@
const fs = require('fs')
const crypto = require('crypto')
const http = require('http')
+const https = require('https') // for webhook requests
const { WebSocketServer } = require('ws')
const express = require('express')
const url = require('url')
@@ -267,6 +268,12 @@ const SQL_UPDATE_USER_PASSWORD = SQL("UPDATE users SET password=?, salt=? WHERE
const SQL_UPDATE_USER_LAST_SEEN = SQL("INSERT OR REPLACE INTO user_last_seen (user_id,atime) VALUES (?,julianday())")
const SQL_UPDATE_USER_IS_BANNED = SQL("update users set is_banned=? where name=?")
+const SQL_SELECT_WEBHOOK = SQL("SELECT * FROM webhooks WHERE user_id=?")
+const SQL_SELECT_WEBHOOK_SEND = SQL("SELECT url, prefix FROM webhooks WHERE user_id=? AND error is null")
+const SQL_UPDATE_WEBHOOK = SQL("INSERT OR REPLACE INTO webhooks (user_id, url, prefix, error) VALUES (?,?,?,null)")
+const SQL_UPDATE_WEBHOOK_ERROR = SQL("UPDATE webhooks SET error=? WHERE user_id=?")
+const SQL_DELETE_WEBHOOK = SQL("DELETE FROM webhooks WHERE user_id=?")
+
const SQL_FIND_TOKEN = SQL("SELECT token FROM tokens WHERE user_id=? AND julianday() < time + 0.004").pluck()
const SQL_CREATE_TOKEN = SQL("INSERT OR REPLACE INTO tokens (user_id,token,time) VALUES (?, lower(hex(randomblob(16))), julianday()) RETURNING token").pluck()
const SQL_VERIFY_TOKEN = SQL("SELECT EXISTS ( SELECT 1 FROM tokens WHERE user_id=? AND julianday() < time + 0.020 AND token=? )").pluck()
@@ -552,6 +559,28 @@ app.get('/unsubscribe', must_be_logged_in, function (req, res) {
res.redirect('/profile')
})
+app.get('/webhook', must_be_logged_in, function (req, res) {
+ req.user.notify = SQL_SELECT_USER_NOTIFY.get(req.user.user_id)
+ let webhook = SQL_SELECT_WEBHOOK.get(req.user.user_id)
+ res.render('webhook.pug', { user: req.user, webhook: webhook })
+})
+
+app.post("/delete-webhook", must_be_logged_in, function (req, res) {
+ SQL_DELETE_WEBHOOK.run(req.user.user_id)
+ res.redirect("/webhook")
+})
+
+app.post("/update-webhook", must_be_logged_in, function (req, res) {
+ let url = req.body.url
+ let prefix = req.body.prefix
+ SQL_UPDATE_WEBHOOK.run(req.user.user_id, url, prefix)
+ const webhook = SQL_SELECT_WEBHOOK_SEND.get(req.user.user_id)
+ if (webhook)
+ send_webhook(req.user.user_id, webhook, "Test message!")
+ res.setHeader("refresh", "3; url=/webhook")
+ res.send("Testing Webhook. Please wait...")
+})
+
app.get('/change-name', must_be_logged_in, function (req, res) {
res.render('change_name.pug', { user: req.user })
})
@@ -1218,6 +1247,7 @@ function annotate_games(games, user_id, unread) {
app.get('/profile', must_be_logged_in, function (req, res) {
req.user.notify = SQL_SELECT_USER_NOTIFY.get(req.user.user_id)
+ req.user.webhook = SQL_SELECT_WEBHOOK.get(req.user.user_id)
res.render('profile.pug', { user: req.user })
})
@@ -1669,6 +1699,92 @@ app.get('/replay-debug/:game_id', function (req, res) {
})
/*
+ * WEBHOOK NOTIFICATIONS
+ */
+
+const webhook_options = {
+ method: "POST",
+ timeout: 6000,
+ headers: {
+ "Content-Type": "application/json"
+ }
+}
+
+function on_webhook_success(user_id) {
+ console.log("WEBHOOK SENT", user_id)
+}
+
+function on_webhook_error(user_id, error) {
+ console.log("WEBHOOK FAIL", user_id, error)
+ SQL_UPDATE_WEBHOOK_ERROR.run(error, user_id)
+}
+
+function send_webhook(user_id, webhook, message) {
+ try {
+ const data = JSON.stringify({ content: webhook.prefix + " " + message })
+ const req = https.request(webhook.url, webhook_options, res => {
+ if (res.statusCode === 200 || res.statusCode === 204)
+ on_webhook_success(user_id)
+ else
+ on_webhook_error(user_id, res.statusCode + " " + http.STATUS_CODES[res.statusCode])
+ })
+ req.on("timeout", () => {
+ on_webhook_error(user_id, "Timeout")
+ req.abort()
+ })
+ req.on("error", (err) => {
+ on_webhook_error(user_id, err.toString())
+ })
+ req.write(data)
+ req.end()
+ } catch (err) {
+ on_webhook_error(user_id, err.message)
+ }
+}
+
+function webhook_game_link(game, user) {
+ if (user.role)
+ return `${SITE_URL}/${game.title_id}/play:${game.game_id}:${encodeURI(user.role)}`
+ return `${SITE_URL}/join/${game.game_id}`
+}
+
+function webhook_ready_to_start(user, game_id) {
+ let webhook = SQL_SELECT_WEBHOOK_SEND.get(user.user_id)
+ if (webhook) {
+ let game = SQL_SELECT_GAME_VIEW.get(game_id)
+ let message = webhook_game_link(game, user) + " - Ready to start!"
+ send_webhook(user.user_id, webhook, message)
+ }
+}
+
+function webhook_game_started(user, game_id) {
+ let webhook = SQL_SELECT_WEBHOOK_SEND.get(user.user_id)
+ if (webhook) {
+ let game = SQL_SELECT_GAME_VIEW.get(game_id)
+ let message = webhook_game_link(game, user) + " - Started!"
+ send_webhook(user.user_id, webhook, message)
+ }
+}
+
+function webhook_game_over(user, game_id) {
+ let webhook = SQL_SELECT_WEBHOOK_SEND.get(user.user_id)
+ if (webhook) {
+ let game = SQL_SELECT_GAME_VIEW.get(game_id)
+ let message = webhook_game_link(game, user) + " - Finished!"
+ send_webhook(user.user_id, webhook, message)
+ }
+}
+
+function webhook_your_turn(user, game_id) {
+ let webhook = SQL_SELECT_WEBHOOK_SEND.get(user.user_id)
+ if (webhook) {
+ let game = SQL_SELECT_GAME_VIEW.get(game_id)
+ let message = webhook_game_link(game, user) + " - Your turn!"
+ send_webhook(user.user_id, webhook, message)
+ }
+}
+
+/*
* MAIL NOTIFICATIONS
*/
@@ -1799,34 +1915,44 @@ function mail_your_turn_notification_to_offline_users(game_id, old_active, activ
let players = SQL_SELECT_PLAYERS.all(game_id)
for (let p of players) {
- if (p.notify) {
- let p_was_active = (old_active === p.role || old_active === 'Both' || old_active === 'All')
- let p_is_active = (active === p.role || active === 'Both' || active === 'All')
- if (!p_was_active && p_is_active) {
- if (is_online(game_id, p.user_id)) {
+ let p_was_active = (old_active === p.role || old_active === 'Both' || old_active === 'All')
+ let p_is_active = (active === p.role || active === 'Both' || active === 'All')
+ if (!p_was_active && p_is_active) {
+ if (is_online(game_id, p.user_id)) {
+ if (p.notify)
reset_your_turn_notification(p, game_id)
- } else {
- mail_your_turn_notification(p, game_id, 15 * MINUTES)
- }
} else {
- reset_your_turn_notification(p, game_id)
+ if (p.notify)
+ mail_your_turn_notification(p, game_id, 15 * MINUTES)
+ webhook_your_turn(p, game_id)
}
+ } else {
+ if (p.notify)
+ reset_your_turn_notification(p, game_id)
}
}
}
function mail_game_started_notification_to_offline_users(game_id, owner_id) {
let players = SQL_SELECT_PLAYERS.all(game_id)
- for (let p of players)
- if (p.notify && !is_online(game_id, p.user_id))
- mail_game_started_notification(p, game_id)
+ for (let p of players) {
+ if (!is_online(game_id, p.user_id)) {
+ if (p.notify)
+ mail_game_started_notification(p, game_id)
+ webhook_game_started(p, game_id)
+ }
+ }
}
function mail_game_over_notification_to_offline_users(game_id, result, victory) {
let players = SQL_SELECT_PLAYERS.all(game_id)
- for (let p of players)
- if (p.notify && !is_online(game_id, p.user_id))
- mail_game_over_notification(p, game_id, result, victory)
+ for (let p of players) {
+ if (!is_online(game_id, p.user_id)) {
+ if (p.notify)
+ mail_game_over_notification(p, game_id, result, victory)
+ webhook_game_over(p, game_id)
+ }
+ }
}
function notify_your_turn_reminder() {
@@ -1843,6 +1969,7 @@ function notify_ready_to_start_reminder() {
if (owner) {
if (owner.notify)
mail_ready_to_start_notification(owner, game.game_id, 25 * HOURS)
+ webhook_ready_to_start(owner, game.game_id)
}
}
}
diff --git a/views/profile.pug b/views/profile.pug
index 7ed8eff..99c8f16 100644
--- a/views/profile.pug
+++ b/views/profile.pug
@@ -28,6 +28,17 @@ html
br
| <a href="/delete-account">Delete account</a>
+ if !user.webhook
+ p <a href="/webhook">Configure webhook</a>
+ else if user.webhook.error
+ dl
+ dt <a href="/webhook">Configure webhook</a>
+ dd.error ERROR: #{user.webhook.error}
+ else
+ dl
+ dt <a href="/webhook">Configure webhook</a>
+ dd= new URL(user.webhook.url).hostname
+
p <a href="/chat">Chat log</a>
p
diff --git a/views/webhook.pug b/views/webhook.pug
new file mode 100644
index 0000000..bf7673f
--- /dev/null
+++ b/views/webhook.pug
@@ -0,0 +1,46 @@
+//- vim:ts=4:sw=4:
+doctype html
+html(lang="en")
+ head
+ include head
+ title Webhook
+ body
+ include header
+ article
+
+ h1 Webhook
+
+ - var url = webhook && webhook.url || ""
+ - var prefix = webhook && webhook.prefix || ""
+
+ form(action="/update-webhook" method="post")
+ if webhook && webhook.error
+ p.error ERROR: #{webhook.error}
+ p Webhook URL:
+ br
+ input#url(type="text" name="url" size=120 placeholder="https://discord.com/api/webhooks/..." value=url required)
+ p Message prefix:
+ br
+ input#prefix(type="text" name="prefix" size=40 placeholder="<@123456789>" value=prefix)
+
+ if webhook
+ button(type="submit") Update
+ else
+ button(type="submit") Create
+
+ if webhook
+ form(action="/delete-webhook" method="post")
+ button(type="submit") Delete
+
+ h2 Discord Notifications
+
+ p You can send notifications to a given channel on a Discord server.
+
+ ol
+ li Create your own server or use an existing server where you have administrator privileges.
+ li <a href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks">Get the webhook URL</a> for the Discord channel where you want notifications to be sent. Enter it into the Webhook URL field.
+ li Find your <a href="https://support.playhive.com/discord-user-id/">Discord User ID</a>. Enter it into the Message prefix field as "&lt;@UserID&gt;"
+
+ h2 Custom Notifications
+
+ p You can integrate with any server that accepts inbound webhooks by setting the webhook URL to the appropriate endpoint. The webhook payload is a JSON object with a "content" property containing the notification message.