diff --git a/README.md b/README.md index 8330e0d..9479673 100644 --- a/README.md +++ b/README.md @@ -8,17 +8,24 @@ A CLI-based client for Discord inspired by [SDF](https://sdf.org)'s [commode](ht ## Usage 1. `pnpm i` 2. `node src/index.js ` +Your token will be then stored in `.comcordrc` after the first launch. -Currently only bot accounts are supported, and that is unlikely to change anytime soon. -~~Eris has a lot of user-only endpoints implemented, but it would require hacking apart Eris to do the things nessicary to spoof being the actual client.~~ -(I have no idea currently what the state of user support in Oceanic is) -I also don't want to give skids an easy point of reference of how to spoof the client. :^) +### User Accounts +User accounts are *partially* supported via `allowUserAccounts=true` in your `.comcordrc`. +This is use at your own risk, despite spoofing the official client. I am not responsible for any banned accounts. +#### Guild members not populating +This is due to Oceanic not implementing Lazy Guilds as they are user account specific. **DO NOT bother Oceanic to implement it!** They are purely a bot-focused library. + +If you are willing to implement Lazy Guilds based off of [unofficial documentation](https://luna.gitlab.io/discord-unofficial-docs/lazy_guilds.html) +and my already existing horrible hacks to make user accounts work in the first place, feel free to send a PR (on GitLab, GitHub repo is a read only mirror). + +### Bot Accounts (prefered) You **MUST** grant your bot all Privileged Gateway Intents. ## Design Decisions * Node.js was chosen currently due to familiarity. -* ~~Eris~~ Oceanic was chosen due to familiarity and the nature of everything not being abstracted out to 200 different classes unlike discord.js. +* Oceanic was chosen due to familiarity and the nature of everything not being abstracted out to 200 different classes unlike discord.js. * "Jank" by design. While I don't expect anyone to actually use comcord on serial terminals or teletypes other than for meme factor, the option is still there. ## TODO @@ -39,6 +46,9 @@ You **MUST** grant your bot all Privileged Gateway Intents. - [x] Clear (c) - [ ] Surf channels forwards (>) - [ ] Surf channels backwards (<) + - [x] AFK toggle (A) + - [x] Send DM (s) + - [x] Answer DM (a) - [x] Message Receiving - [x] Markdown styling - [ ] Common markdown (bold, italic, etc) @@ -54,8 +64,9 @@ You **MUST** grant your bot all Privileged Gateway Intents. - [x] Puts incoming messages into queue whilst in send mode - [ ] Mentions - [ ] Replies -- [ ] Configuration - - [ ] Default guild/channel +- [x] Configuration + - [x] Default guild/channel + - No way to set in client (yet?), `defaultChannel=` and `defaultGuild=` in your `.comcordrc`. - [ ] Threads -- [ ] Not have the token just be in argv +- [x] Not have the token just be in argv - [x] Not have everything in one file diff --git a/src/commands/afk.js b/src/commands/afk.js index 7a3e396..70fc347 100644 --- a/src/commands/afk.js +++ b/src/commands/afk.js @@ -3,6 +3,7 @@ const {updatePresence} = require("../lib/presence"); addCommand("A", "toggles AFK mode", function () { if (comcord.state.afk == true) { + comcord.state.afk = false; comcord.client.shards.forEach((shard) => (shard.presence.afk = false)); comcord.client.editStatus("online"); console.log(""); diff --git a/src/commands/listUsers.js b/src/commands/listUsers.js index 394117b..27f1bd8 100644 --- a/src/commands/listUsers.js +++ b/src/commands/listUsers.js @@ -40,21 +40,22 @@ function listUsers() { ); const online = [...guild.members.values()].filter((m) => m.presence); - online.sort((a, b) => a.name - b.name); + online.sort((a, b) => a.tag.localeCompare(b.tag)); let longest = 0; for (const member of online) { - const name = member.user.tag; + const name = member.tag; if (name.length + 3 > longest) longest = name.length + 3; } - const columns = Math.ceil(process.stdout.columns / longest); + const columns = Math.floor(process.stdout.columns / longest); let index = 0; for (const member of online) { - const name = member.user.tag; + const name = member.tag; const status = getStatus(member.presence.status); - const nameAndStatus = chalk.reset(name) + status; + const nameAndStatus = + (member.user.bot ? chalk.yellow(name) : chalk.reset(name)) + status; index++; process.stdout.write( diff --git a/src/commands/privateMessages.js b/src/commands/privateMessages.js new file mode 100644 index 0000000..b7d0d6a --- /dev/null +++ b/src/commands/privateMessages.js @@ -0,0 +1,51 @@ +const chalk = require("chalk"); + +const {addCommand} = require("../lib/command"); +const {startPrompt} = require("../lib/prompt"); +const {listUsers} = require("./listUsers"); + +function startDM(user) { + startPrompt(":msg> ", async function (input) { + if (input == "") { + console.log(`\n`); + } else { + try { + const channel = await user.createDM(); + await channel.createMessage({content: input}); + console.log(chalk.bold.green(`\n`)); + } catch (err) { + console.log("\n"); + } + } + }); +} + +addCommand("s", "send private", function () { + console.log("Provide a RECIPIENT"); + startPrompt(":to> ", function (who) { + let target; + for (const user of comcord.client.users.values()) { + if (user.tag == who) { + target = user; + break; + } + } + + if (target) { + console.log(""); + startDM(target); + } else { + listUsers(); + } + }); +}); + +addCommand("a", "answer a send", function () { + if (comcord.state.lastDM) { + console.log(chalk.bold.green(``)); + startDM(comcord.state.lastDM); + } else { + // FIXME: figure out the actual message in com + console.log(""); + } +}); diff --git a/src/commands/switchGuild.js b/src/commands/switchGuild.js index 542b954..c072a63 100644 --- a/src/commands/switchGuild.js +++ b/src/commands/switchGuild.js @@ -56,3 +56,5 @@ function switchGuild(input) { addCommand("G", "goto guild", function () { startPrompt(":guild> ", switchGuild); }); + +module.exports = {switchGuild}; diff --git a/src/index.js b/src/index.js index 367043f..8399763 100644 --- a/src/index.js +++ b/src/index.js @@ -1,15 +1,36 @@ -const {Client} = require("oceanic.js"); -const chalk = require("chalk"); +const {Client, Constants} = require("oceanic.js"); const DiscordRPC = require("discord-rpc"); +const chalk = require("chalk"); +const fs = require("fs"); + +const rcfile = require("./lib/rcfile"); +const config = {}; + +if (fs.existsSync(rcfile.path)) { + console.log("% Reading " + rcfile.path + " ..."); + rcfile.readFile(config); +} const CLIENT_ID = "1026163285877325874"; const token = process.argv[2]; +if (!config.token && token) { + console.log("% Writing token to .comcordrc"); + config.token = token; + rcfile.writeFile(config); +} + +if (!config.token && !token) { + console.log("No token provided."); + process.exit(1); +} process.title = "comcord"; global.comcord = { + config, state: { + rpcConnected: false, startTime: Date.now(), currentGuild: null, currentChannel: null, @@ -22,21 +43,28 @@ global.comcord = { commands: {}, }; const client = new Client({ - auth: "Bot " + token, + auth: + (config.allowUserAccounts == "true" ? "" : "Bot ") + + (token ?? config.token), defaultImageFormat: "png", defaultImageSize: 1024, gateway: { intents: ["ALL"], - activities: [ - { - name: "comcord", - type: "GAME", - application_id: CLIENT_ID, - timestamps: { - start: comcord.state.startTime, + maxShards: 1, + concurrency: 1, + presence: { + status: "online", + activities: [ + { + name: "comcord", + type: 0, + application_id: CLIENT_ID, + timestamps: { + start: comcord.state.startTime, + }, }, - }, - ], + ], + }, }, }); comcord.client = client; @@ -53,10 +81,11 @@ require("./commands/help"); const {sendMode} = require("./commands/send"); require("./commands/emote"); const {listGuilds} = require("./commands/listGuilds"); -require("./commands/switchGuild"); // loads listChannels and listUsers +const {switchGuild} = require("./commands/switchGuild"); // loads listChannels and listUsers require("./commands/switchChannel"); //loads listUsers require("./commands/history"); // includes extended history require("./commands/afk"); +require("./commands/privateMessages"); process.stdin.setRawMode(true); process.stdin.resume(); @@ -70,20 +99,44 @@ client.once("ready", function () { listGuilds(); - rpc - .login({ - clientId: CLIENT_ID, - }) - .catch(function () {}); + if (config.defaultGuild) { + const guild = client.guilds.get(config.defaultGuild); + if (guild != null) { + if (config.defaultChannel) { + comcord.state.currentChannel = config.defaultChannel; + comcord.state.lastChannel.set( + config.defaultGuild, + config.defaultChannel + ); + } + switchGuild(guild.name); + } else { + console.log("% This account is not in the defined default guild."); + } + } else { + if (config.defaultChannel) { + console.log("% Default channel defined without defining default guild."); + } + } + + if (client.user.bot) { + rpc + .login({ + clientId: CLIENT_ID, + }) + .catch(function () {}); + } }); client.on("error", function () {}); rpc.on("connected", function () { + comcord.state.rpcConnected = true; updatePresence(); }); let retryingRPC = false; rpc.once("ready", function () { rpc.transport.on("close", function () { + comcord.state.rpcConnected = false; if (!retryingRPC) { retryingRPC = true; setTimeout(function () { @@ -102,21 +155,55 @@ rpc.once("ready", function () { }); rpc.on("error", function () {}); -client.on("messageCreate", function (msg) { +client.on("messageCreate", async function (msg) { if (msg.author.id === client.user.id) return; - if (msg.channel.id == comcord.state.currentChannel) { + if (msg.channelID && !msg.channel) { + try { + const dmChannel = await msg.author.createDM(); + if (dmChannel.id === msg.channelID) { + msg.channel = dmChannel; + } + } catch { + // + } + } + + if ( + (msg.channel ? msg.channel.id : msg.channelID) == + comcord.state.currentChannel || + msg.channel?.recipient != null + ) { if (comcord.state.inPrompt) { comcord.state.messageQueue.push(msg); } else { processMessage(msg); } } + + if (msg.channel?.recipient != null) { + comcord.state.lastDM = msg.author; + } }); -client.on("messageUpdate", function (msg, old) { +client.on("messageUpdate", async function (msg, old) { if (msg.author.id === client.user.id) return; - if (msg.channel.id == comcord.state.currentChannel) { + if (msg.channelID && !msg.channel) { + try { + const dmChannel = await msg.author.createDM(); + if (dmChannel.id === msg.channelID) { + msg.channel = dmChannel; + } + } catch { + // + } + } + + if ( + (msg.channel ? msg.channel.id : msg.channelID) == + comcord.state.currentChannel || + msg.channel?.recipient != null + ) { if (msg.content == old.content) return; if (comcord.state.inPrompt) { @@ -125,6 +212,10 @@ client.on("messageUpdate", function (msg, old) { processMessage(msg); } } + + if (msg.channel?.recipient != null) { + comcord.state.lastDM = msg.author; + } }); process.stdin.on("data", async function (key) { @@ -158,7 +249,89 @@ process.stdin.on("data", async function (key) { } }); -client.connect(); +if ( + config.allowUserAccounts == "true" && + !(token ?? config.token).startsWith("Bot ") +) { + if (fetch == null) { + console.log("Node v18+ needed for user account support."); + process.exit(1); + } + + (async function () { + comcord.clientSpoof = require("./lib/clientSpoof"); + const superProperties = await comcord.clientSpoof.getSuperProperties(); + + console.log("% Allowing non-bot tokens to connect"); + const connectLines = client.connect.toString().split("\n"); + connectLines.splice(0, 4); + connectLines.splice(-1, 1); + + const newConnect = new client.connect.constructor(connectLines.join("\n")); + client.connect = newConnect.bind(client); + + // gross hack + global.Constants_1 = Constants; + try { + global.Erlpack = require("erlpack"); + } catch { + global.Erlpack = false; + } + + console.log("% Injecting headers into request handler"); + client.rest.handler.options.userAgent = `Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) discord/${superProperties.client_version} Chrome/91.0.4472.164 Electron/13.6.6 Safari/537.36`; + client.rest.handler._request = client.rest.handler.request.bind( + client.rest.handler + ); + client.rest.handler.request = async function (options) { + options.headers = options.headers ?? {}; + options.headers["X-Super-Properties"] = + await comcord.clientSpoof.getSuperPropertiesBase64(); + + return await this._request.apply(this, [options]); + }.bind(client.rest.handler); + + console.log("% Setting gateway connection properties"); + client.shards.options.connectionProperties = superProperties; + + console.log("% Injecting application into READY payload"); + client.shards._spawn = client.shards.spawn.bind(client.shards); + client.shards.spawn = function (id) { + const res = this._spawn.apply(this, [id]); + const shard = this.get(id); + if (shard) { + shard._onDispatch = shard.onDispatch.bind(shard); + shard.onDispatch = async function (packet) { + if (packet.t == "READY") { + packet.d.application = {id: CLIENT_ID, flags: 565248}; + } + + const ret = await this._onDispatch.apply(this, [packet]); + + if (packet.t == "READY") { + for (const guild of packet.d.guilds) { + await this._onDispatch.apply(this, [ + { + t: "GUILD_CREATE", + d: guild, + }, + ]); + } + } + + return ret; + }.bind(shard); + } + + return res; + }.bind(client.shards); + + console.log("% Connecting to gateway now"); + await client.connect(); + })(); +} else { + client.connect(); +} console.log("COMcord (c)left 2022"); console.log("Type 'h' for Commands"); diff --git a/src/lib/clientSpoof.js b/src/lib/clientSpoof.js new file mode 100644 index 0000000..d67cf17 --- /dev/null +++ b/src/lib/clientSpoof.js @@ -0,0 +1,107 @@ +/* + * This single file is **EXCLUDED** from the project license. + * + * (c) 2022 Cynthia Foxwell, all rights reserved. + * Permission is hereby granted to redistribute this file ONLY with copies of comcord. + * You may not reverse engineer, modify, copy, or redistribute this file for any other uses outside of comcord. + */ + +const os = require("os"); + +async function fetchMainPage() { + const res = await fetch("https://discord.com/channels/@me"); + return await res.text(); +} + +async function fetchAsset(assetPath) { + return await fetch("https://discord.com/" + assetPath).then((res) => + res.text() + ); +} + +const MATCH_SCRIPT = '