Ruining Your Mafia Game: An Ethan Jones Story
Public demo of 'JonesBot:' - a robot that uses a now-publicly-known site exploit to construct lists of likely mafia. Concept, construction, results. Was it effective? How can we prevent this behavior in the future? Why does Ethan Jones keep trying to ruin the mafia subforum?
The Exploit
"Last Post" on player profiles updates to "just now" when they post in a private thread (such as mafia PM chat). The activity feed containing recent posts does not.
We can append .json to either of these pages to see a json readout of exactly what time their "last post" was, compare this to the timestamp on the latest post in the activity feed, and map exactly which time different players are PMing.
This exploit has already been brought to the attention of the makers of Discourse in order to hopefully get it patched. In the meantime, we can use chatzy or private profiles to avoid ruining mafia games. I recommend chatzy over privating every profile because private profiles can't be ISO'd and prevent the functioning of non-malicious mafia bots.
Reconstructing the Bot
Glossary
AWS Cloud, AWS CLI, AWS SDK
-Buzzwords for people who want to pretend they're better than you. Don't let them get away with it!
Lambda
-"Serverless" function that lives on AWS which we can call and pass arguments
-also a buzzzword
Cloudwatch
-Allows us to write rules which execute on a schedule to invoke and pass args to our Lambdas
-buzzword
DynamoDB
-"No Sequel" "Database" - whatever that means
API
-Connection point for us to receive data and send data to the discourse website programmatically
Code
const AWS = require('aws-sdk');
AWS.config.update({
region: "us-east-1"
});
class JonesBot {
main(username) {
console.log("Running...");
this.getLastPost(username).then(res1 => {
this.getLastVisible(username).then(res2 => {
console.log(res1)
console.log(res2)
if (res1 != res2) {
console.log("Hey " + username)
this.postToDynamo(username, res1)
}
})
})
}
postToDynamo(username, res1) {
let ddb = new AWS.DynamoDB({apiVersion: '2012-10-08'});
let params = {
TableName: 'jonesBot7',
Item: {
'username' : {S: username},
'lastPosted' : {S: res1}
}
}
ddb.putItem(params, function(err, data) {
if (err) {
console.log("Dynamo error: ", err)
} else {
console.log("Dynamo success: ", data)
}
})
}
getLastPost(username) {
const request1 = `https://namafia.com/u/${username}.json`;
let lastPosted;
var d1;
var d2;
return fetch(request1, {
method: 'GET',
mode: 'cors',
credentials: 'include',
headers: {
"Content-Type": "application/json; charset=utf-8"
}
})
.then(res => {return res.json()})
.then(res => {return lastPosted = res.user.last_posted_at})
}
getLastVisible(username) {
const request2 = `https://namafia.com/user_actions.json?username=${username}`;
let lastVisiblePostTime;
return fetch(request2, {
method: 'GET',
mode: 'cors',
credentials: 'include',
headers: {
"Content-Type": "application/json; charset=utf-8"
}
})
.then(res => {return res.json()})
.then(res => {for(let i = 0; i<res.user_actions.length; i++) {
if (res.user_actions[i].acting_username == username && res.user_actions[i].username == username) {
return res.user_actions[i].created_at
}
}})
.then(res => {return lastVisiblePostTime = res})
}
}
exports.lambdaHandler = function(input, context = null) {
let username = input.username;
let jsbot = new JonesBot();
jsbot.main(username);
}
const fetch = require('isomorphic-fetch')
const AWS = require('aws-sdk');
var Q = require('q');
const uuidv1 = require('uuid/v1');
AWS.config.update({
region: "us-east-1"
});
class DynamoScan {
scanDynamo() {
let ddb = new AWS.DynamoDB();
let params = {
TableName: 'jonesBot7'
};
return Q.nfcall(ddb.scan.bind(ddb), params).then(function(data) {
return data;
});
}
writeData(data) {
let dynamoOutput = {};
for(let i = 0; i < data.Items.length; i++) {
let name = data.Items[i].username.S
if(!dynamoOutput[name]) {
dynamoOutput[name] = 1;
} else {
dynamoOutput[name]++;
}
}
return dynamoOutput;
}
sortData(data) {
let keys = []
for(let key in data) {
if (data.hasOwnProperty(key)) {
keys.push(key);
}
}
for(let i = 0; i < keys.length; i++) {
let temp = keys[i];
let j = i - 1;
while(j >= 0 && data[keys[j]] > data[temp]){
keys[j + 1] = keys[j]
j--;
}
keys[j + 1] = temp;
}
let string = "";
for (let i = 0; i<keys.length; i++) {
string += keys[i]
string += "\n"
}
string += uuidv1();
return string;
}
postToWebsite(params) {
const request = `https://namafia.com/posts.json`
fetch(request, {
method: "POST",
body: JSON.stringify(params),
headers: {
"Content-Type": "application/json"
},
credentials: "same-origin"
})
}
}
exports.lambdaHandler = function(input, context = null) {
let dynamoScan = new DynamoScan();
dynamoScan.scanDynamo()
.then(res => {
let data = dynamoScan.sortData(dynamoScan.writeData(res));
let params = {
"topic_id": 1755,
"raw": data,
"archetype": "private_message",
"created_at": "2001-09-11"
}
dynamoScan.postToWebsite(params);
})
}