Skip to content
This repository has been archived by the owner on Feb 9, 2022. It is now read-only.

Commit

Permalink
Added getMods and getVips functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
RAnders00 committed Sep 21, 2020
1 parent c6d5307 commit 26e7ecf
Show file tree
Hide file tree
Showing 5 changed files with 302 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unversioned

- Minor: Added `getMods` and `getVips` to Chat client API to easily fetch a list of mods and VIPs for a channel.

## v4.0.2

- Bugfix: Fixed a single server message resolving/rejecting more than one promise (e.g. cases where many messages were sent to the same channel, the first success/error response would resolve/reject all the waiting promises) (#32, #65)
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,11 @@ You probably will want to use these functions on `ChatClient` most frequently:
from the bot.
- `client.setColor(color: Color)` - set the username color of your bot account.
E.g. `client.setColor({ r: 255, g: 0, b: 127 })`.
- `client.getMods(channelName: string)` and `client.getVips(channelName: string)` -
Get a list of moderators/VIPs in a channel. Returns
a promise that resolves to an array of strings (login names of the moderators/VIPs).
Note that due to Twitch's restrictions, this function cannot be used with anonymous chat clients.
(The request will time out if your chat client is logged in as anonymous.)
Extra functionality:
Expand Down
11 changes: 11 additions & 0 deletions lib/client/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as debugLogger from "debug-logger";
import { getMods, getVips } from "../operations/get-mods-vips";
import { ClientConfiguration } from "../config/config";
import { Color } from "../message/color";
import { ClientMixin, ConnectionMixin } from "../mixins/base-mixin";
Expand Down Expand Up @@ -210,6 +211,16 @@ export class ChatClient extends BaseClient {
await setColor(this.requireConnection(), color);
}

public async getMods(channelName: string): Promise<string[]> {
validateChannelName(channelName);
return await getMods(this.requireConnection(), channelName);
}

public async getVips(channelName: string): Promise<string[]> {
validateChannelName(channelName);
return await getVips(this.requireConnection(), channelName);
}

public async ping(): Promise<void> {
await sendPing(this.requireConnection());
}
Expand Down
178 changes: 178 additions & 0 deletions lib/operations/get-mods-vips.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { assert } from "chai";
import * as sinon from "sinon";
import { TimeoutError } from "../await/timeout-error";
import { ClientError, ConnectionError } from "../client/errors";
import { assertErrorChain, fakeConnection } from "../helpers.spec";
import { getMods, GetUsersError, getVips } from "./get-mods-vips";

describe("./operations/join", function () {
describe("#getMods()", function () {
it("sends the correct wire command", function () {
sinon.useFakeTimers(); // prevent the promise timing out
const { data, client } = fakeConnection();
getMods(client, "pajlada");
assert.deepEqual(data, ["PRIVMSG #pajlada :/mods\r\n"]);
});

it("resolves on incoming no_mods", async function () {
const { emitAndEnd, client, clientError } = fakeConnection();

const promise = getMods(client, "tmijs");

emitAndEnd(
"@msg-id=no_mods :tmi.twitch.tv NOTICE #tmijs :There are no moderators of this channel."
);

assert.deepStrictEqual(await promise, []);
await clientError;
});

it("resolves on incoming room_mods", async function () {
const { emitAndEnd, client, clientError } = fakeConnection();

const promise = getMods(client, "randers");

emitAndEnd(
"@msg-id=room_mods :tmi.twitch.tv NOTICE #randers :The moderators of this channel are: test, abc, def"
);

assert.deepStrictEqual(await promise, ["test", "abc", "def"]);
await clientError;
});

it("resolves on incoming room_mods (just 1 mod)", async function () {
const { emitAndEnd, client, clientError } = fakeConnection();

const promise = getMods(client, "randers");

emitAndEnd(
"@msg-id=room_mods :tmi.twitch.tv NOTICE #randers :The moderators of this channel are: test"
);

assert.deepStrictEqual(await promise, ["test"]);
await clientError;
});

it("should fail on timeout of 2000 ms", async function () {
// given
sinon.useFakeTimers();
const { client, clientError } = fakeConnection();

// when
const promise = getMods(client, "test");

// then
sinon.clock.tick(2000);
await assertErrorChain(
promise,
GetUsersError,
"Failed to get mods of channel test: Timed out after waiting for res" +
"ponse for 2000 milliseconds",
TimeoutError,
"Timed out after waiting for response for 2000 milliseconds"
);

await assertErrorChain(
clientError,
GetUsersError,
"Failed to get mods of channel test: Timed out after waiting for res" +
"ponse for 2000 milliseconds",
TimeoutError,
"Timed out after waiting for response for 2000 milliseconds"
);
});
});

describe("#getVips()", function () {
it("sends the correct wire command", function () {
sinon.useFakeTimers(); // prevent the promise timing out
const { data, client } = fakeConnection();
getVips(client, "pajlada");
assert.deepEqual(data, ["PRIVMSG #pajlada :/vips\r\n"]);
});

it("resolves on incoming no_vips", async function () {
const { emitAndEnd, client, clientError } = fakeConnection();

const promise = getVips(client, "tmijs");

emitAndEnd(
"@msg-id=no_vips :tmi.twitch.tv NOTICE #tmijs :This channel does not have any VIPs."
);

assert.deepStrictEqual(await promise, []);
await clientError;
});

it("resolves on incoming vips_success", async function () {
const { emitAndEnd, client, clientError } = fakeConnection();

const promise = getVips(client, "randers");

emitAndEnd(
"@msg-id=vips_success :tmi.twitch.tv NOTICE #randers :The VIPs of this channel are: eeya_, pajlada, pastorbruce, ragglefraggle, supervate, supibot."
);

assert.deepStrictEqual(await promise, [
"eeya_",
"pajlada",
"pastorbruce",
"ragglefraggle",
"supervate",
"supibot",
]);
await clientError;
});

it("resolves on incoming room_mods (just 1 mod)", async function () {
const { emitAndEnd, client, clientError } = fakeConnection();

const promise = getVips(client, "randers");

emitAndEnd(
"@msg-id=vips_success :tmi.twitch.tv NOTICE #randers :The VIPs of this channel are: supibot."
);

assert.deepStrictEqual(await promise, ["supibot"]);
await clientError;
});

it("should fail on timeout of 2000 ms", async function () {
// given
sinon.useFakeTimers();
const { client, clientError } = fakeConnection();

// when
const promise = getVips(client, "test");

// then
sinon.clock.tick(2000);
await assertErrorChain(
promise,
GetUsersError,
"Failed to get vips of channel test: Timed out after waiting for res" +
"ponse for 2000 milliseconds",
TimeoutError,
"Timed out after waiting for response for 2000 milliseconds"
);

await assertErrorChain(
clientError,
GetUsersError,
"Failed to get vips of channel test: Timed out after waiting for res" +
"ponse for 2000 milliseconds",
TimeoutError,
"Timed out after waiting for response for 2000 milliseconds"
);
});
});

describe("GetUsersError", function () {
it("should not be instanceof ConnectionError", function () {
assert.notInstanceOf(new GetUsersError("test", "mods"), ConnectionError);
});
it("should not be instanceof ClientError", function () {
assert.notInstanceOf(new GetUsersError("test", "mods"), ClientError);
});
});
});
106 changes: 106 additions & 0 deletions lib/operations/get-mods-vips.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { awaitResponse } from "../await/await-response";
import { matchingNotice } from "../await/conditions";
import { SingleConnection } from "../client/connection";
import { MessageError } from "../client/errors";
import { NoticeMessage } from "../message/twitch-types/notice";

interface GetUsersConfig {
command: "mods" | "vips";
msgIdError: string;
msgIdNone: string;
msgIdSome: string;
someMessagePrefix: string;
someMessageSuffix: string;
}

const getModsInfo: GetUsersConfig = {
command: "mods",
msgIdError: "usage_mods",
msgIdNone: "no_mods",
msgIdSome: "room_mods",
someMessagePrefix: "The moderators of this channel are: ",
someMessageSuffix: "",
};
const getVipsInfo: GetUsersConfig = {
command: "vips",
msgIdError: "usage_vips",
msgIdNone: "no_vips",
msgIdSome: "vips_success",
someMessagePrefix: "The VIPs of this channel are: ",
someMessageSuffix: ".",
};

export class GetUsersError extends MessageError {
public channelName: string;
public type: "mods" | "vips";

public constructor(
channelName: string,
type: "mods" | "vips",
message?: string,
cause?: Error
) {
super(message, cause);
this.channelName = channelName;
this.type = type;
}
}

export async function getMods(
conn: SingleConnection,
channelName: string
): Promise<string[]> {
return await getModsOrVips(conn, channelName, getModsInfo);
}

export async function getVips(
conn: SingleConnection,
channelName: string
): Promise<string[]> {
return await getModsOrVips(conn, channelName, getVipsInfo);
}

async function getModsOrVips(
conn: SingleConnection,
channelName: string,
config: GetUsersConfig
): Promise<string[]> {
conn.sendRaw(`PRIVMSG #${channelName} :/${config.command}`);

const responseMsg = (await awaitResponse(conn, {
success: matchingNotice(channelName, [config.msgIdNone, config.msgIdSome]),
failure: matchingNotice(channelName, [config.msgIdError]),
errorType: (msg, cause) =>
new GetUsersError(channelName, config.command, msg, cause),
errorMessage: `Failed to get ${config.command} of channel ${channelName}`,
})) as NoticeMessage;

if (responseMsg.messageID === config.msgIdNone) {
return [];
}

if (responseMsg.messageID === config.msgIdSome) {
let text = responseMsg.messageText;

if (
!text.startsWith(config.someMessagePrefix) ||
!text.endsWith(config.someMessageSuffix)
) {
throw new GetUsersError(
channelName,
config.command,
`Failed to get ${config.command} of channel ${channelName}: Response message had unexpected format: ${responseMsg.rawSource}`
);
}

// slice away the prefix and suffix
text = text.slice(
config.someMessagePrefix.length,
text.length - config.someMessageSuffix.length
);

return text.split(", ");
}

throw new Error("unreachable");
}

0 comments on commit 26e7ecf

Please sign in to comment.