Initial commit

master
seckl 2022-03-14 08:10:46 +01:00
commit 33ae414247
10 changed files with 914 additions and 0 deletions

13
.cdb.json Normal file
View File

@ -0,0 +1,13 @@
{
"type": "MOD",
"title": "Web chat",
"name": "webchat",
"short_description": "Makes the in-game chat accessible through a web browser.",
"tags": ["chat"],
"license": "LGPLv3",
"media_license": "LGPLv3",
"long_description": "This mod makes the in-game chat accessible through a web browser. The mod itself implements the Minetest server-side functions. It also contains example PHP code for the web chat GUI and the interaction with the Minetest server.",
"repo": "https://gitlab.com/seckl/webchat.git",
"website": "https://gitlab.com/seckl/webchat/",
"issue_tracker": "https://gitlab.com/seckl/webchat/-/issues"
}

47
README.md Normal file
View File

@ -0,0 +1,47 @@
# Minetest Webchat Mod
This mod makes the [Minetest](https://www.minetest.net/) [in-game chat](https://wiki.minetest.net/Chat) accessible through a web browser. The mod basically:
* adds some functions to interact with the Minetest in-game chat over a network socket.
* modifies the /msg and /me chat commands to work with the web chat.
* saves the in-game chat and some player data in local files. A locally installed web server can access the data and send it to the web users.
## Limitations and known bugs
* Currently the mod is very simple. It can only be used from a locally running web server, because the web server needs access to some files written by the mod.
* The browsers actively poll the chat for new messages. This causes some network and server load when multiple users are logged in at the same time.
# Installation
* Install the Webchat mod as described in the [Minetest Wiki](https://wiki.minetest.net/Installing_Mods).
* To access the Webchat functions over a network socket you also need to install the [Mineysocket](https://github.com/miney-py/mineysocket) mod.
* Set up a web server (e.g. [Apache](https://httpd.apache.org/)) on the same machine and copy the files in the directory `www/` to the DocumentRoot directory (e.g. `/var/www/html/`).
* Point $webchatdir in index.php to the Webchat data directory. Of course you can also modify the look & feel of the web pages as you like.
* Since the web server user (e.g. `www-data`) needs read access to some files in the data directory, you may need to adjust the access rights accordingly.
# Usage
To use the web chat, you just have to log in with your Minetest player name and password, no additional account is needed. You must set a password for your player in the game, empty passwords are not allowed.
After logging in you see the public chat on the left and your direct messages on the right side. You can hide and unhide both by clicking on the buttons below.
Type your messages in the input line below the chats. The web chat works just like the [in-game chat](https://wiki.minetest.net/Chat#Sending_messages):
* Just type your message and hit enter for public messages
* `/msg <player> <message>` for a direct message (DM) to a single player
* `/me <message>` to prepend your player name in front of a public message
Your messages will be sent to all players in the game and in the web chat.
On the bottom of the window the server messages are shown, e.g. when a player joins the game or the web chat.
## Example
The following screenshots shows the web chat running on a desktop and a smartphone.
![Screenshots](screenshots.png)
# License
[LGPL-3.0-or-later](https://spdx.org/licenses/LGPL-3.0-or-later.html)

362
init.lua Normal file
View File

@ -0,0 +1,362 @@
--
-- Webchat-Mod
-- © 2022-03-12 secklsurvival@gerloni.net
-- GPL-3.0-or-later
--
-- To use the functions outside this Mod
webchat={}
-- Mod dir
local webchatdir=minetest.get_worldpath().."/"..minetest.get_current_modname()
-- Player data
local pldatadir=webchatdir.."/playerdata"
-- Files in playerdata/<plname>/ directories:
-- lastseen 1611343962 game Contains timestamp (Unix epoch) and type of last activity
-- game_is_online webchat_is_online 1609018688 File exists only while player is online and contains the timestamp of his last activity
-- game_player.log website_player.log 1611349823 join Log of player game/website events
minetest.mkdir(webchatdir)
-- Append new message to webchat log
function webchat_send_msg(plname,msg)
local clog_file=io.open(webchatdir.."/chat.log","a")
clog_file:write(os.time().."\t"..plname.."\t"..msg.."\n")
clog_file:close()
end
-- Append new message to DM log of sender and receiver
function dmlog_send_msg(frpl,topl,msg)
local frpldat_dir=pldatadir.."/"..frpl
minetest.mkdir(frpldat_dir)
local dmlog_frpl_file=io.open(frpldat_dir.."/dm.log","a")
dmlog_frpl_file:write(os.time().."\tto\t"..topl.."\t"..msg.."\n")
dmlog_frpl_file:close()
if topl ~= "+error+" then
local topldat_dir=pldatadir.."/"..topl
minetest.mkdir(topldat_dir)
local dmlog_topl_file=io.open(topldat_dir.."/dm.log","a")
dmlog_topl_file:write(os.time().."\tfrom\t"..frpl.."\t"..msg.."\n")
dmlog_topl_file:close()
end
end
-- Append new message to server log
function server_log(msg)
local slog_file=io.open(webchatdir.."/server.log","a")
slog_file:write(os.time().."\t"..msg.."\n")
slog_file:close()
end
-- Update playerdata when player tries to join the game
local function game_prejoin(plname,plip)
local pldat_dir=pldatadir.."/"..plname
minetest.mkdir(pldat_dir)
local game_plog_file=io.open(pldat_dir.."/game_player.log","a")
game_plog_file:write(os.time().."\tprejoin\t"..plip.."\n")
game_plog_file:close()
end
-- Update playerdata when player joined the game
local function game_join(plname)
server_log(plname.." joined the game.")
local pldat_dir=pldatadir.."/"..plname
minetest.mkdir(pldat_dir)
local game_plog_file=io.open(pldat_dir.."/game_player.log","a")
game_plog_file:write(os.time().."\tjoin\t"..minetest.get_player_ip(plname).."\n")
game_plog_file:close()
local lastseen_file=io.open(pldat_dir.."/lastseen","w")
lastseen_file:write(os.time().."\tgame\n")
lastseen_file:close()
local game_online_file=io.open(pldat_dir.."/game_is_online","w")
game_online_file:write(os.time().."\n")
game_online_file:close()
end
-- Update playerdata when player left the game
local function game_leave(action,plname)
server_log(plname.." left the game.")
local pldat_dir=pldatadir.."/"..plname
minetest.mkdir(pldat_dir)
local game_plog_file=io.open(pldat_dir.."/game_player.log","a")
game_plog_file:write(os.time().."\t"..action.."\n")
game_plog_file:close()
local lastseen_file=io.open(pldat_dir.."/lastseen","w")
lastseen_file:write(os.time().."\tgame\n")
lastseen_file:close()
os.remove(pldat_dir.."/game_is_online")
end
-- Remove *_is_online files on startup/shutdown in case they were left behind
local function cleanup()
for _,plname in ipairs(minetest.get_dir_list(pldatadir,true)) do
local pldat_dir=pldatadir.."/"..plname
os.remove(pldat_dir.."/game_is_online")
os.remove(pldat_dir.."/webchat_is_online")
end
end
-- Player plname is logged in on the website and has the chat page open in his browser
function webchat.ping(plname)
local pldat_dir=pldatadir.."/"..plname
minetest.mkdir(pldat_dir)
local lastseen_file=io.open(pldat_dir.."/lastseen","w")
lastseen_file:write(os.time().."\twebchat\n")
lastseen_file:close()
local webchat_online_file=io.open(pldat_dir.."/webchat_is_online","r")
if webchat_online_file then
webchat_online_file:close()
else
minetest.chat_send_all("*** "..plname.." joined the web chat.")
server_log(plname.." joined the web chat.")
local website_plog_file=io.open(pldat_dir.."/website_player.log","a")
website_plog_file:write(os.time().."\tchat_join\n")
website_plog_file:close()
minetest.log("action",plname.." joined the webchat.")
end
webchat_online_file=io.open(pldat_dir.."/webchat_is_online","w")
webchat_online_file:write(os.time().."\n")
webchat_online_file:close()
-- If 600 sec later the file webchat_is_online is older than 540 sec we suppose that
-- the user lost the connection or closed the browser without logging out
minetest.after(600,function()
local pldat_dir=pldatadir.."/"..plname
local webchat_online_file=io.open(pldat_dir.."/webchat_is_online","r")
if webchat_online_file and os.time()-webchat_online_file:read() > 540 then
webchat_online_file:close()
minetest.chat_send_all("*** "..plname.." left the web chat (timeout).")
server_log(plname.." left the web chat (timeout).")
os.remove(pldat_dir.."/webchat_is_online")
local website_plog_file=io.open(pldat_dir.."/website_player.log","a")
website_plog_file:write(os.time().."\tchat_timeout\n")
website_plog_file:close()
minetest.log("action",plname.." left the webchat (timeout).")
end
end)
end
-- Modify /me & /msg chat commands so that they also write to the webchat log (original Minetest code: builtin/game/chat.lua)
minetest.register_chatcommand("me",{
params="<action>",
description="Show chat action (e.g., '/me orders a pizza' displays '<player name> orders a pizza')",
privs={shout=true},
func=function(plname,param)
minetest.chat_send_all("* "..plname.." "..param)
webchat_send_msg(plname,"* "..plname.." "..param)
return true
end
})
minetest.register_chatcommand("msg",{
params="<sender> <message>",
description="Send a direct message to a player (game and web chat)",
privs={shout=true},
func=function(sender,param)
local sendto,message=param:match("^(%S+)%s(.+)$")
if not sendto then
return false,"Invalid usage, see /help msg."
end
if not minetest.player_exists(sendto) then
return false,"The player "..sendto.." does not exist."
end
-- Always send DM to webchat dm log of sender and receiver
dmlog_send_msg(sender,sendto,message)
-- Send DM to game chat only if the receiver is online
if minetest.get_player_by_name(sendto) then
minetest.chat_send_player(sendto,"DM from "..sender..": "..message)
end
minetest.log("action","DM from "..sender.." to "..sendto.." (in game): "..message)
return true,"Message sent."
end
})
-- Login to website with Minetest credentials
function webchat.login(plname,passwd,plip)
local pl_ah=minetest.get_auth_handler().get_auth(plname)
if pl_ah and minetest.check_password_entry(plname,pl_ah['password'],passwd) then
local pldat_dir=pldatadir.."/"..plname
minetest.mkdir(pldat_dir)
local website_plog_file=io.open(pldat_dir.."/website_player.log","a")
website_plog_file:write(os.time().."\tlogin\t"..plip.."\n")
website_plog_file:close()
minetest.log("action",plname.." ["..plip.."] logged in to the website.")
return "auth-ok"
else
minetest.log("action","Server: User "..plname.." at "..plip.." supplied wrong password on the website.")
return "auth-failed"
end
end
-- Logout from website
function webchat.logout(plname)
local pldat_dir=pldatadir.."/"..plname
local website_plog_file=io.open(pldat_dir.."/website_player.log","a")
if os.remove(pldat_dir.."/webchat_is_online") then
minetest.chat_send_all("*** "..plname.." left the web chat.")
server_log(plname.." left the web chat.")
website_plog_file:write(os.time().."\tchat_leave\n")
end
website_plog_file:write(os.time().."\tlogout\n")
website_plog_file:close()
minetest.log("action",plname.." logged out from the website.")
end
-- Receive message from webchat - Partly taken from builtin/game/chat.lua
function webchat.receive_msg(sender,msg)
if msg:sub(1,1) ~= "/" then
-- msg does not start with / => chat message
if minetest.check_player_privs(sender,"shout") then
minetest.chat_send_all("<"..sender.."> "..msg)
webchat_send_msg(sender,msg)
minetest.log("action","Chat message from "..sender.." (in webchat): "..msg)
else
dmlog_send_msg(sender,"+error+","You don't have permission to shout.")
end
else
-- msg starts with a / => command
local cmd,param=string.match(msg,"^/([^ ]+) *(.*)")
param=param or ""
if not cmd then
dmlog_send_msg(sender,"+error+","Empty command.")
return false
elseif cmd=="me" then
if minetest.check_player_privs(sender,"shout") then
minetest.chat_send_all("* "..sender.." "..param)
webchat_send_msg(sender,"* "..sender.." "..param)
minetest.log("action","/me message from "..sender.." (in webchat): ".."* "..sender.." "..param)
else
dmlog_send_msg(sender,"+error+","/me - You don't have permission to run this command (missing privilege: shout).")
end
elseif cmd=="msg" then
if minetest.check_player_privs(sender,"shout") then
local sendto,message=param:match("^(%S+)%s(.+)$")
if not sendto then
dmlog_send_msg(sender,"+error+","/msg - Invalid usage.")
minetest.log("action","Invalid usage of command /msg by "..sender.." (in webchat).")
return false
end
if not minetest.player_exists(sendto) then
dmlog_send_msg(sender,"+error+","/msg - The player "..sendto.." does not exist.")
minetest.log("action","DM from "..sender.." to non-existent player "..sendto.." (in webchat).")
return false
end
-- Always send DM to webchat dm log of sender and receiver
dmlog_send_msg(sender,sendto,message)
-- Send DM to game chat only if the receiver/sender is online
if minetest.get_player_by_name(sendto) then
minetest.chat_send_player(sendto,"DM from "..sender..": "..message)
end
if minetest.get_player_by_name(sender) then
minetest.chat_send_player(sender,"DM to "..sendto..": "..message)
end
minetest.log("action","DM from "..sender.." to "..sendto.." (in webchat): "..message)
else
dmlog_send_msg(sender,"+error+","/msg - You don't have permission to run this command (missing privilege: shout).")
end
else
dmlog_send_msg(sender,"+error+","/"..cmd.." - Unknown command.")
minetest.log("action","Unknown command "..cmd.." from "..sender.." (in webchat).")
return false
end
end
webchat.ping(sender)
end
--------------------------------------------------------------------------------
-- Game event registration
--------------------------------------------------------------------------------
-- Update playerdata/ on startup
cleanup()
server_log("Server startup")
-- Update chatlog on every chat message
minetest.register_on_chat_message(webchat_send_msg)
-- Update playerdata/ on (pre)join and leave
minetest.register_on_prejoinplayer(function(plname,plip)
game_prejoin(plname,plip)
end)
minetest.register_on_joinplayer(function(player)
game_join(player:get_player_name())
end)
minetest.register_on_leaveplayer(function(player)
game_leave("leave",player:get_player_name())
end)
-- Update playerdata/ on shutdown
minetest.register_on_shutdown(function()
for _,player in ipairs(minetest.get_connected_players()) do
game_leave("shutdown",player:get_player_name())
end
cleanup()
server_log("Server shutdown")
end)
--------------------------------------------------------------------------------

3
mod.conf Normal file
View File

@ -0,0 +1,3 @@
name = webchat
optional_depends = mineysocket
description = Functions to create a Webchat e.g. in PHP.

BIN
screenshots.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 KiB

308
www/index.php Normal file
View File

@ -0,0 +1,308 @@
<?php
session_start();
// Webchat data dir
$webchatdir='/var/games/minetest-server/.minetest/worlds/SecklSurvival/webchat';
// Generate a unique color for every username based on its crc32 hash sum
function playercolor($player,$color) {
list($r1,$r2,$g1,$g2,$b1,$b2)=str_split(dechex(crc32($player)),1);
if ($color=="r") {
return 'ff'.$g1.$g2.$b1.$b2;
} elseif ($color=="g") {
return $r1.$r2.'ff'.$b1.$b2;
} elseif ($color=="b") {
return $r1.$r2.$g1.$g2.'ff';
}
}
if (empty($_SESSION['player'])) {
// Not logged in => Please login first
header('Location: /login/');
exit();
} elseif (isset($_GET['lastchatline']) and isset($_GET['lastservline'])) {
// Send/update chat history to browser
$mt_socket=socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($mt_socket,'127.0.0.1','29999');
$cmd=json_encode(array('lua'=>"webchat.ping('".$_SESSION['player']."')",'id'=>'chat_ping'))."\n";
socket_write($mt_socket,$cmd,strlen($cmd));
$reply=socket_read($mt_socket,1048576);
socket_close($mt_socket);
$chatdata=array();
$chatlines=0;
$loglines=file($webchatdir.'/chat.log');
foreach ($loglines as $chatline) {
$chatlines++;
// Return all lines after $_GET['lastchatline']
if ($chatlines > $_GET['lastchatline']) {
// Edit line
list($timestamp,$user,$text)=explode("\t",htmlspecialchars(trim($chatline),ENT_QUOTES));
$date=date('Y-m-d',$timestamp);
$time=date('H:i:s T',$timestamp);
$color=playercolor($user,"b");
$chatline='<tr style="color: #'.$color.'"><td id="u">&lt;'.$user.'&gt;</td><td><span id="d">&nbsp;'.$date.'</span><span id="t">&nbsp;'.$time.'</span></td></tr><tr style="color: #'.$color.'"><td id="txt" colspan="2">'.$text.'</td></tr>';
array_push($chatdata,$chatline);
}
}
$dmdata=array();
$dmlines=0;
$loglines=file($webchatdir.'/playerdata/'.$_SESSION['player'].'/dm.log');
foreach ($loglines as $dmline) {
$dmlines++;
// Return all lines after $_GET['lastdmline']
if ($dmlines > $_GET['lastdmline']) {
// Edit line
list($timestamp,$fromto,$user,$text)=explode("\t",htmlspecialchars(trim($dmline),ENT_QUOTES));
$date=date('Y-m-d',$timestamp);
$time=date('H:i:s T',$timestamp);
if ($user=="+error+") {
if ($timestamp > time()-300) {
// Show only error messages from the last 5 min
$color='ff0000';
$dmline='<tr style="color: #'.$color.'"><td id="err" colspan="2"><dfn class="wchat-tooltip" wchat-tooltip-text="Valid commands are: /msg &lt;player&gt; &lt;message&gt;, /me &lt;message&gt;">Error: '.$text.'</dfn></td></tr>';
array_push($dmdata,$dmline);
}
} else {
$color=playercolor($user,"r");
$dmline='<tr style="color: #'.$color.'"><td id="u">DM '.$fromto.' '.$user.':</td><td><span id="d">&nbsp;'.$date.'</span><span id="t">&nbsp;'.$time.'</span></td></tr><tr style="color: #'.$color.'"><td id="txt" colspan="2">'.$text.'</td></tr>';
array_push($dmdata,$dmline);
}
}
}
$servdata=array();
$servlines=0;
$loglines=file($webchatdir.'/server.log');
foreach ($loglines as $servline) {
$servlines++;
// Return all lines after $_GET['lastservline']
if ($servlines > $_GET['lastservline']) {
// Edit line
list($timestamp,$text)=explode("\t",htmlspecialchars(trim($servline),ENT_QUOTES));
$date=date('Y-m-d',$timestamp);
$time=date('H:i:s T',$timestamp);
$servline='<tr><td id="txt">'.$text.'</td><td><span id="d">&nbsp;'.$date.'</span><span id="t">&nbsp;'.$time.'</span></td></tr>';
array_push($servdata,$servline);
}
}
echo json_encode(array("chatlines"=>$chatlines,"servlines"=>$servlines,"dmlines"=>$dmlines,"chatdata"=>$chatdata,"servdata"=>$servdata,"dmdata"=>$dmdata));
exit();
} elseif (!empty($_POST['msg'])) {
// Send chat message
$mt_socket=socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($mt_socket,'127.0.0.1','29999');
// Sanitize $message
$message=addslashes($_POST['msg']); // Escape some special characters ('"\)
$message=trim($message," \n\r\t\v\0\\"); // Remove some special chars from the beginning and end
$message=substr($message,0,500); // Max length 500 chars
$cmd=json_encode(array('lua'=>"webchat.receive_msg('".$_SESSION['player']."','".$message."')",'id'=>'chat_msg'))."\n";
socket_write($mt_socket,$cmd,strlen($cmd));
$reply=socket_read($mt_socket,1048576);
socket_close($mt_socket);
// Send HTTP 204 so that the browser stays on the page
ob_start();
header("HTTP/1.1 204 NO CONTENT");
header("Cache-Control: no-cache, no-store, must-revalidate");
header("Pragma: no-cache");
header("Expires: 0");
ob_end_flush();
exit();
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Minetest Web Chat</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/style.css">
<script src="jquery.min.js"></script>
<script>
function ToggleChat() {
var chlog=document.getElementById("wchat-chatlog");
var chbut=document.getElementById("wchat-chswbt-ch");
var dmlog=document.getElementById("wchat-dmlog");
var dmbut=document.getElementById("wchat-chswbt-dm");
if (chlog.style.display==="none") {
chlog.style.display="flex";
chlog.style.width="50%";
chlog.style.float="left";
chbut.style.color="#000";
chbut.style.background="#2c9cdc";
dmlog.style.display="flex";
dmlog.style.width="50%";
dmbut.style.color="#000";
dmbut.style.background="#2c9cdc";
} else {
chlog.style.display="none";
chbut.style.color="inherit";
chbut.style.background="inherit";
dmlog.style.display="flex";
dmlog.style.width="100%";
dmbut.style.color="#000";
dmbut.style.background="#2c9cdc";
}
}
function ToggleDM() {
var dmlog=document.getElementById("wchat-dmlog");
var dmbut=document.getElementById("wchat-chswbt-dm");
var chlog=document.getElementById("wchat-chatlog");
var chbut=document.getElementById("wchat-chswbt-ch");
if (dmlog.style.display==="none") {
chlog.style.display="flex";
chlog.style.width="50%";
chlog.style.float="left";
chbut.style.color="#000";
chbut.style.background="#2c9cdc";
dmlog.style.display="flex";
dmlog.style.width="50%";
dmbut.style.color="#000";
dmbut.style.background="#2c9cdc";
} else {
chlog.style.display="flex";
chlog.style.width="100%";
chlog.style.float="none";
chbut.style.color="#000";
chbut.style.background="#2c9cdc";
dmlog.style.display="none";
dmbut.style.color="inherit";
dmbut.style.background="inherit";
}
}
function playSound(url) {
const audio=new Audio(url);
audio.play();
}
function scrollDown() {$("html, body").animate({scrollTop: $(document).height()}, "fast")}
function SelectChatMsg() {document.getElementsByName("msg")[0].select()}
function Logout() {window.location.replace('/login/?logout')}
lastChatLine=0;
lastDmLine=0;
lastServLine=0;
scrollOnce=true;
$(document).ready(function() {
// Scroll to bottom once after the page has loaded
scrollDown();
});
// Update the chat/dm/server history
$.ajaxSetup({timeout: 1500}); // Timeout 1.5 sec for getJSON
setInterval("updateHist()",2000); // Update history every 2 sec
function updateHist() {
$.getJSON('?lastchatline='+lastChatLine+'&lastdmline='+lastDmLine+'&lastservline='+lastServLine,function(data) {
if (lastChatLine!=data.chatlines || lastDmLine!=data.dmlines || lastServLine!=data.servlines) {
// Play sound and scroll to bottom on incoming message
playSound('snd-msg.ogg');
scrollDown();
}
lastChatLine=data.chatlines;
lastDmLine=data.dmlines;
lastServLine=data.servlines;
$.each(data.chatdata,function(key,value) {
$("#wchat-chatlog").append(''+value);
});
$.each(data.dmdata,function(key,value) {
$("#wchat-dmlog").append(''+value);
});
$.each(data.servdata,function(key,value) {
$("#wchat-servlog").append(''+value);
});
// Scroll to bottom once when chat history has been loaded
if (scrollOnce) {
scrollDown();
scrollOnce=false;
}
});
}
</script>
</head>
<body>
<div class="wchat-content">
<h3 id="help">Minetest Web Chat</h3>
<p>The chat works the same as the Minetest in-game chat:
<ul>
<li>Chat and /me messages will be sent to all connected players (Minetest and Web Chat).</li>
<li>Direct messages (DM) to one player can be sent with /msg &lt;player&gt; &lt;message&gt;.</li>
</ul>
See the <a href="https://wiki.minetest.net/Chat#Sending_messages" target="_blank">Minetest Wiki</a> for details.
Chat and /me messages are shown on the left side, DM on the right.
</p>
<p>
You can hide and unhide the columns by clicking on the green buttons below.
</p>
<table class="wchat-chatout" id="wchat-chatlog"></table>
<table class="wchat-chatout" id="wchat-dmlog"></table>
<table id="wchat-chatsw"><tr><td class="wchat-chswbt" id="wchat-chswbt-ch" onclick="ToggleChat()">Chat</td><td class="wchat-chswbt" id="wchat-chswbt-dm" onclick="ToggleDM()"><span id="long">Direct Messages</span><span id="short">DM</span></td></tr></table>
<table id="wchat-chatin"><tbody>
<tr><td>
<form method="POST" action="" onsubmit="SelectChatMsg()">
<?php
echo ' <p id="u">&lt;'.$_SESSION['player'].'&gt;&nbsp;&nbsp;<a href="#help">?</a></p>'."\n";
?>
<p><input type="text" required minlength="1" name="msg" placeholder="Type your message here and press enter..." autofocus autocomplete="off"></p>
<p><input style="width: 65%; margin-right: 5px" type="submit" value="Send"><input style="width: calc(100% - 65% - 10px); margin-left: 5px" type="button" value="Logout" onclick="Logout()"></p>
</form>
</td></tr>
</tbody></table>
<table class="wchat-chatout" id="wchat-servlog"></table>
</div>
</body>
</html>

4
www/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

117
www/login/index.php Normal file
View File

@ -0,0 +1,117 @@
<?php
session_start();
function goback() {
header('Location: /');
exit();
}
if (isset($_GET['logout'])) {
// Logout
if (!empty($_SESSION['player'])) {
$mt_socket=socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($mt_socket,'127.0.0.1','29999');
$cmd=json_encode(array('lua'=>"webchat.logout('".$_SESSION['player']."')",'id'=>'web_logout'))."\n";
socket_write($mt_socket,$cmd,strlen($cmd));
$reply=socket_read($mt_socket,1048576);
socket_close($mt_socket);
}
// Delete session cookie
$cp=session_get_cookie_params();
setcookie(session_name(),'',0,$cp['path'],$cp['domain'],$cp['secure'],isset($cp['httponly']));
// Delete and close session
session_destroy();
session_write_close();
goback();
} elseif (!empty($_SESSION['player'])) {
// Already logged in
goback();
} elseif (!empty($_POST['player']) and !empty($_POST['pass'])) {
// Login attempt
// Sanitize $player,$passwd
$player=preg_replace('/[^A-Za-z0-9-_]/','',$_POST['player']); // Allow only A-Za-z0-9-_
$player=substr($player,0,19); // Max length 19 chars
$player=rtrim($player," \n\r\t\v\0\\"); // Remove some trailing special chars
$passwd=addslashes($_POST['pass']); // Escape some special characters ('"\)
$passwd=substr($passwd,0,99); // Max length 99 chars
$passwd=rtrim($passwd,"\n\r\t\v\0\\"); // Remove some trailing special chars
$mt_socket=socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($mt_socket,'127.0.0.1','29999');
$cmd=json_encode(array('lua'=>"return webchat.login('".$player."','".$passwd."','".$_SERVER['REMOTE_ADDR']."')",'id'=>'web_login'))."\n";
socket_write($mt_socket,$cmd,strlen($cmd));
$reply=socket_read($mt_socket,1048576);
socket_close($mt_socket);
if (isset(json_decode($reply)->{'result'}[0]) and json_decode($reply)->{'result'}[0]=="auth-ok") {
// Login successful
$_SESSION['player']=$player;
goback();
} else {
// Wrong player or password
header("HTTP/1.1 401 UNAUTHORIZED");
$retry=true;
}
} elseif (isset($_GET['from'])) {
header("HTTP/1.1 401 UNAUTHORIZED");
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Minetest Web Chat Login</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/style.css">
</script>
</head>
<body>
<div class="wchat-content">
<h3>Minetest Web Chat Login</h3>
<table id="wchat-chatin"><tbody>
<tr><td>
<form method="POST" action="">
<p>Please log in with your Minetest player name and password.</p>
<p id="u">In case you did not set a password for your player yet please do it now, empty passwords are not allowed.</p>
<p>Player name: </p><p><input type="text" required minlength="1" name="player" placeholder="Enter player name and press enter..." autofocus></p>
<p>Password: </p><p><input type="password" required minlength="1" name="pass" placeholder="Enter password and press enter..."></p>
<?php
if (!empty($retry)) {
echo ' <p id="u">Login failed. Make sure you entered the correct player name/password and that you have the shout privilege. Please try again.</p>'."\n";
}
?>
<p><input type="submit" value="Login"></p>
</form>
</td></tr>
</tbody></table>
</div>
</body>
</html>

BIN
www/snd-msg.ogg Normal file

Binary file not shown.

60
www/style.css Normal file
View File

@ -0,0 +1,60 @@
/* Elements */
html {height: 100%; box-sizing: border-box; font-family: Verdana,sans-serif; font-size: 15px; color: #2c9cdc; background-color: #000}*,*:before,*:after{box-sizing: inherit}
footer {display: block; padding-top: 16px; padding-bottom: 16px; border-top: 1px solid; margin-top: 24px; text-align: center}
a {color: inherit}
b {font-weight: bolder}
h3 {font-weight: 400; font-size: 24px; border-bottom: 1px solid; padding-bottom: 4px; margin-top: 5px; margin-bottom: 5px}
img {border-style: none; vertical-align: middle; max-width: 100%; height: auto}
/* Classes */
.wchat-button {border: none; display: inline-block; height: 36px; padding: 10px 8px; text-decoration: none; cursor: pointer}
.wchat-button:hover {color: #000; background-color: #2c9cdc}
.wchat-chswbt {color: #000; background-color: #2c9cdc; border: none; display: inline-block; height: 36px; padding: 10px 8px; text-decoration: none; cursor: pointer}
.wchat-nobutton {margin-top: 0; margin-bottom: 0; border: none; display: inline-block; height: 36px; padding: 10px 8px; text-decoration: none}
.wchat-content {margin-left: auto; margin-right: auto; height: calc(100% - 124px); max-width: 1600px; padding: 8px}
.wchat-chatout {font-family: monospace; padding: 8px; border-collapse: collapse; line-height: 1; border: 1px solid; overflow: hidden; display: flex; flex-direction: column-reverse; overflow-wrap: anywhere}
.wchat-chatout tr {display: block}
.wchat-chatout td {white-space: nowrap}
.wchat-chatout #u {width: 100%; font-weight: bolder}
.wchat-chatout #d, #t {text-align: right; font-size: 0.75em}
.wchat-chatout #txt {white-space: normal; width: 100%; padding-left: 10px; padding-top: 0; padding-bottom: 5px}
.wchat-chatout #err {white-space: normal; width: 100%; padding: 0; padding-bottom: 5px; display: block}
.wchat-tooltip {cursor: help; display: block}
[wchat-tooltip-text] {position: relative}
[wchat-tooltip-text]::after {content: attr(wchat-tooltip-text); pointer-events: none; opacity: 0; transition: opacity 0.5s; display: block; position: absolute; bottom: 1.5em; padding: 0.5em; z-index: 1; background-color: #000; border: solid 1px; border-radius: 0.5em}
[wchat-tooltip-text]:hover::after {opacity: 1}
/* IDs */
#wchat-chatlog {height: 500px; width: 50%; float: left}
#wchat-dmlog {height: 500px; width: 50%}
#wchat-servlog {max-height: 100px}
#wchat-chatsw {width: 100%; border: 1px solid; font-family: monospace; font-weight: bolder; margin-top: 5px; margin-bottom: 5px}
#wchat-chatsw td {width: 50%; border: 1px solid; text-align: center}
#wchat-chswbt-dm #long {display: inherit}
#wchat-chswbt-dm #short {display: none}
#wchat-chatin {font-family: monospace; border-collapse: collapse; line-height: 1; border: 1px solid; clear: both; display: flex; margin-top: 5px; margin-bottom: 5px}
#wchat-chatin tbody {width: 100%}
#wchat-chatin tr {display: flex}
#wchat-chatin td {width: 100%; padding: 1px 10px}
#wchat-chatin p {width: 100%; margin: 10px 0px}
#wchat-chatin #u {font-weight: bolder}
#wchat-chatin input {width: 100%; font-family: inherit; font-size: inherit; color: inherit; background-color: #000; padding: 12px}
/* Hide/change some elements on small screens */
@media screen and (max-width: 850px){
.wchat-chatout #d {display: none}
}
/* Hide/change some elements on very small screens */
@media screen and (max-width: 500px){
.wchat-chatout #t {display: none}
#wchat-chatsw #long {display: none}
#wchat-chatsw #short {display: inherit}
}