Skip to content
This repository was archived by the owner on Nov 10, 2021. It is now read-only.

Activity Ping #26

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,9 @@ export type Config = {
leaderboardCooldown: number,
commandChannels?: any
},
vcPing?: any
vcPing?: any,
activePing?: {
timeout: number,
roles: any
}
}
7 changes: 7 additions & 0 deletions config.json.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,12 @@
"vcPing": {
"guild1": "role1",
"guild2": "role2"
},
"activePing": {
"timeout": 900000,
"roles": {
"guild1": "role1",
"guild2": "role2"
}
}
}
97 changes: 97 additions & 0 deletions helpers/active.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Collection, GuildMember, RoleResolvable, Snowflake } from "discord.js";
import { config, client } from "..";
import { get, getLastUserMessageTimestamp, getLastUserReactionTimestamp, getUserEnabled, set } from "../util/levels";

export const activeConfig = config.activePing;
const memberActiveTimeouts = new Collection<Snowflake, NodeJS.Timeout>();

export function readyActive() {
if (!activeConfig) return;

for (const guildId in activeConfig.roles) {
const guild = client.guilds.cache.get(guildId);
let role = guild.roles.cache.get(activeConfig.roles[guildId]);
const memberCollection = guild.members.cache;
memberCollection.forEach(async member => {
shouldHaveRole(member, role, await isMemberActive(member));
});
}
}

export function setupActiveListeners() {
if (!activeConfig) return;

client.on('message', message => memberActive(message.member));
client.on('messageUpdate', message => memberActive(message.member));
client.on('messageReactionAdd', (reaction, user) => {
memberActive(reaction.message.guild.members.cache.get(user.id));
set(`${user.id}:react`, new Date().getTime());
});
client.on('messageReactionRemove', (reaction, user) => {
memberActive(reaction.message.guild.members.cache.get(user.id));
set(`${user.id}:react`, new Date().getTime());
});
client.on('guildMemberAdd', memberUpdate);
client.on('presenceUpdate', (old, presence) => memberUpdate(presence.member));
client.on('voiceStateUpdate', state => memberUpdate(state.member));
}

export function shouldHaveRole(member:GuildMember, role:RoleResolvable, shouldHaveRole:boolean) {
let r = typeof role == 'object' ? role : member.guild.roles.cache.get(role);
if (shouldHaveRole) {
if (!member.roles.cache.has(r.id)) {
console.log(`Attempting to give ${member.displayName} @${r.name}`);
member.roles.add(role).then(()=>{
console.log(`Gave ${member.displayName} @${r.name}`);
}).catch((e)=>{
console.warn(`Could not give ${member.displayName} @${r.name}!`, e);
});
}
} else {
if (member.roles.cache.has(r.id)) {
console.log(`Attempting to remove @${r.name} from ${member.displayName}`);
member.roles.remove(role).then(()=>{
console.log(`Removed @${r.name} from ${member.displayName}`);
}).catch((e)=>{
console.warn(`Could not remove ${member.displayName} from @${r.name}!`, e);
});
}
}
}

async function isMemberActive(member:GuildMember) {
if (!await getUserEnabled(member)) return false;

let online = member.presence.status == 'online'; // If their status is online
let lastMessageTimeDifference = new Date().getTime() - (await getLastUserMessageTimestamp(member.id)).getTime(); // How long ago their last message was sent
let lastReactionTimeDifference = new Date().getTime() - (await getLastUserReactionTimestamp(member.id)).getTime(); // How long ago their last reaction was
let isInVc = member.voice.channel != undefined && !member.voice.deaf; // If they're in VC and not deafened, we can probably assume they're active

return isInVc || (
online && (
lastMessageTimeDifference < activeConfig.timeout ||
lastReactionTimeDifference < activeConfig.timeout
)
);
}

async function memberUpdate(member:GuildMember) {
if (member?.guild.id in activeConfig.roles) {
shouldHaveRole(member, activeConfig.roles[member.guild.id], await isMemberActive(member));
}
}

function memberActive(member:GuildMember) {
if (member?.guild.id in activeConfig.roles) {
shouldHaveRole(member, activeConfig.roles[member.guild.id], true);
if (memberActiveTimeouts.has(member.id)) {
clearTimeout(memberActiveTimeouts.get(member.id));
memberActiveTimeouts.delete(member.id);
}
memberActiveTimeouts.set(member.id,
setTimeout(async (member:GuildMember) => {
shouldHaveRole(member, activeConfig.roles[member.guild.id], await isMemberActive(member));
}, activeConfig.timeout, member)
);
}
}
3 changes: 3 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,22 @@ import { readyMembers, setupMemberListeners } from "./helpers/members";
import { readyVC, setupVCListeners } from "./helpers/vc";
import { setupMessageListeners } from "./helpers/messageHandler";
import { setupReactionListeners } from "./helpers/reactionHandler";
import { readyActive, setupActiveListeners } from "./helpers/active";

client.on('ready', ()=>{
console.log(`Logged in as ${client.user.tag}`);
client.user.setPresence({activity:{'type':'WATCHING',name:'🐐'}});

readyMembers();
readyVC();
readyActive();
});

setupMessageListeners();
setupReactionListeners();

setupVCListeners();
setupMemberListeners();
setupActiveListeners();

client.login(config.token);
47 changes: 47 additions & 0 deletions triggers/commands/active.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { GuildMember, Message, MessageEmbed, MessageMentions } from "discord.js";
import { client } from "../..";
import { Command } from "../../Types";
import { getUsage } from "../../util/commands";
import { get, getUserEnabled, set } from "../../util/levels";
import { getIDFromMention } from "../../util/text";

module.exports = <Command> {
name: 'active',
args: '[status [user]| toggle]',
description: 'Set or view your preference for the @active role...',
guildOnly: true,
minArgs: 0,
async execute(message, args) {
switch (args[0]) {
case 'status':
case undefined:
// Query the user's status
let userMatch = message.content.match(MessageMentions.USERS_PATTERN);
let member = message.member;
if (userMatch) {
member = message.guild.members.cache.get(getIDFromMention(userMatch[0]));
}
let enabled = await getUserEnabled(member);
sendStatus(message, member, enabled);
break;

case 'toggle':
let enabledStatus = !(await getUserEnabled(message.author));
await set(`${message.author.id}:enabled`, enabledStatus);
sendStatus(message, message.member, enabledStatus);
break;

default:
// Throw usage
message.reply(`You must use the command like: \`${getUsage(module.exports)}\``);
}
}
}

function sendStatus(message:Message, member:GuildMember, enabled:boolean) {
message.channel.send(new MessageEmbed()
.setAuthor(member.nickname, member.user.displayAvatarURL())
.setColor(enabled ? '#4caf50' : '#f44336')
.setDescription(`${member}, you **will${enabled?'':' not'}** be included in \`@active\` pings`)
);
}
2 changes: 1 addition & 1 deletion triggers/triggers/levels.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TriggeredCommand } from "../../Types";
import { config } from "../..";
import { rand } from "../../util/math";
import { rand } from "../../util/general";
import { getLevelNumber, getUserLevel, redisClient, setUserLevel } from "../../util/levels";

module.exports = <TriggeredCommand> {
Expand Down
16 changes: 16 additions & 0 deletions util/general.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/** Get a random value within a range of the max and min value in range */
export function rand(range:number[]) {
return Math.random() * (
Math.max(...range) - Math.min(...range)
) + Math.min(...range);
}

/**
* Remove `item` from `array`
* `array` is modified so you don't have to reassign it
*/
export function remove<T>(item:T, array:T[]) {
let index = array.indexOf(item);
array.splice(index, 1);
return array;
}
34 changes: 33 additions & 1 deletion util/levels.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Collection, Snowflake } from "discord.js";
import { Collection, GuildMember, Snowflake, User } from "discord.js";
import { createClient, RedisClient } from "redis";
import { promisify } from "util";
import { client, config } from "..";
Expand Down Expand Up @@ -27,6 +27,38 @@ export async function getUserLevel(uid:Snowflake):Promise<LevelData> {
return data;
}

/**
* Query the databse for the time of a user's last message
* @param uid the ID of the user to query for
* @param failover the value to return if database misses, `Date(0)` if not specified
*/
export async function getLastUserMessageTimestamp(uid:Snowflake, failover = new Date(0)) {
if (!redisClient) return failover;
let lastTimestamp = await get(`${uid}:last`);
return lastTimestamp ? new Date(parseInt(lastTimestamp)) : failover;
}

/**
* Query the databse for the time of a user's last reaction
* @param uid the ID of the user to query for
* @param failover the value to return if database misses, `Date(0)` if not specified
*/
export async function getLastUserReactionTimestamp(uid:Snowflake, failover = new Date(0)) {
if (!redisClient) return failover;
let lastTimestamp = await get(`${uid}:react`);
return lastTimestamp ? new Date(parseInt(lastTimestamp)) : failover;
}

/**
* Query the database for whether the active role is enabled for the user
* @param user the user to search for
*/
export async function getUserEnabled({id}:User|GuildMember) {
let enabledStatus = await get(`${id}:enabled`);
// TODO: make opt-in/out a config option (server-by-server?)
return enabledStatus == null || enabledStatus == 'true'; // Opt-out: `isEnabled == null ||`; opt-in: `isEnabled &&`
}

export function getLevelNumber(xp:number) {
return Math.max(Math.floor(Math.log2(xp / 10)), 0);
}
Expand Down
6 changes: 0 additions & 6 deletions util/math.ts

This file was deleted.

4 changes: 1 addition & 3 deletions util/text.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { Snowflake } from "discord.js";
import { type } from "os";
import { client } from "..";
import { rand } from "./math";
import { rand } from "./general";

export const getTextAfter = (fragment:string, main:string) => main.substr(main.indexOf(fragment) + fragment.length);

Expand Down