diff --git a/.env.example b/.env.example index 650e43f..aa1dbe7 100644 --- a/.env.example +++ b/.env.example @@ -4,5 +4,11 @@ CLIENT_ID=clientId CLIENT_SECRET=clientSecret CHANNELS=channelsOfBot(,separated) DEVELOPERS=userIdOfDeveloper(,separated) + TIDAL_HOST=http://localhost TIDAL_PORT=47836 + +HA_API_URL=http://homeassistant.com/api/states/ +HA_API_TOKEN=Nina hätte hier jetzt ihr Token ausversehen stehen hihi +HA_DESK_SENSOR_ID=entityId +HA_ROOMTEMP_SENSOR_IDS=entityId(,separated) diff --git a/src/commands/collection.ts b/src/commands/collection.ts index cb2ab4c..9b144f5 100644 --- a/src/commands/collection.ts +++ b/src/commands/collection.ts @@ -1,7 +1,11 @@ import { Collection } from "@discordjs/collection"; import { PingCommand } from "./impl/ping.ts"; +import { PositionCommand } from "./impl/position.ts"; import { SongCommand } from "./impl/song.ts"; +import { TempCommand } from "./impl/temp.ts"; import { VanishCommand } from "./impl/vanish.ts"; +import { VolumeCommand } from "./impl/volume.ts"; +import { WhereCommand } from "./impl/where.ts"; import type { ICommand } from "./interface.ts"; export const commands = new Collection(); @@ -10,6 +14,10 @@ const cmds: Array = []; cmds.push(new SongCommand()); cmds.push(new PingCommand()); cmds.push(new VanishCommand()); +cmds.push(new PositionCommand()); +cmds.push(new TempCommand()); +cmds.push(new WhereCommand()); +cmds.push(new VolumeCommand()); for (const command of cmds) { commands.set(command.name, command); diff --git a/src/commands/impl/ping.ts b/src/commands/impl/ping.ts index d0d0239..31b013f 100644 --- a/src/commands/impl/ping.ts +++ b/src/commands/impl/ping.ts @@ -28,7 +28,7 @@ export class PingCommand extends BaseCommand { ) => { logSuccess(`${channel} ${user} ping command triggered`); const uptime = getUptime(this.started, Date.now()); - const message = `uptime: ${uptime}`; + const message = `StickDank uptime: ${uptime}`; this.chatClient.say(channel, message, { replyTo: msg, }); diff --git a/src/commands/impl/position.ts b/src/commands/impl/position.ts new file mode 100644 index 0000000..3495bd3 --- /dev/null +++ b/src/commands/impl/position.ts @@ -0,0 +1,48 @@ +import type { ChatMessage } from "@twurple/chat"; +import { getDeskHeight } from "../../util/api-homeassistant.ts"; +import { logSuccess } from "../../util/logger.ts"; +import { BaseCommand } from "../base-command.ts"; +import type { ICommandRequirements } from "../interface.ts"; + +export class PositionCommand extends BaseCommand { + name = "position"; + cooldown = 0; + enabled = true; + + requirements: ICommandRequirements = { + developer: false, + mod: false, + }; + + triggered = async ( + channel: string, + user: string, + _text: string, + msg: ChatMessage, + ) => { + logSuccess(`${channel} ${user} position command triggered`); + const position = await getPosition(); + this.chatClient.say(channel, `darius is ${position} right now`, { + replyTo: msg, + }); + }; +} + +async function getPosition(): Promise { + const heightFromHA = await getDeskHeight(); + if (!heightFromHA) { + return "unkown"; + } + const height = convertHeightToCentimeters(heightFromHA.state); + if (height > 95) { + return "standing"; + } else { + return "sitting"; + } +} + +function convertHeightToCentimeters(height: string): number { + const meters = parseFloat(height); + const centimeters = Math.round(meters * 100); + return centimeters; +} diff --git a/src/commands/impl/song.ts b/src/commands/impl/song.ts index 8a89a67..a0259c0 100644 --- a/src/commands/impl/song.ts +++ b/src/commands/impl/song.ts @@ -1,21 +1,9 @@ import type { ChatMessage } from "@twurple/chat"; -import axios from "axios"; -import { Config } from "../../config/config.ts"; -import { logSuccess, logWarning } from "../../util/logger.ts"; +import { getSongFromTidalFormatted } from "../../util/api-tidal.ts"; +import { logSuccess } from "../../util/logger.ts"; import { BaseCommand } from "../base-command.ts"; import type { ICommandRequirements } from "../interface.ts"; -interface ISong { - title: string; - artists: string; - album: string; - playingFrom: string; - status: "playing" | "paused"; - url: string; - current: string; - duration: string; -} - export class SongCommand extends BaseCommand { name = "song"; cooldown = 0; @@ -33,7 +21,7 @@ export class SongCommand extends BaseCommand { msg: ChatMessage, ) => { logSuccess(`${channel} ${user} song command triggered`); - const song = await getSongFromTidal(); + const song = await getSongFromTidalFormatted(); if (song) { logSuccess(song); this.chatClient.say(channel, song, { replyTo: msg }); @@ -42,20 +30,3 @@ export class SongCommand extends BaseCommand { } }; } - -async function getSongFromTidal(): Promise { - try { - const response = await axios.get( - `${Config.tidal.host}:${Config.tidal.port}/current`, - ); - - const currentSong = response.data; - - const status = currentSong.status === "playing" ? "▶️" : "⏸️"; - - return `listening to ${currentSong.title} by ${currentSong.artists}. ${status} ${currentSong.current}/${currentSong.duration}. link: ${currentSong.url}`; - } catch { - logWarning("error getting song from tidal"); - return ""; - } -} diff --git a/src/commands/impl/temp.ts b/src/commands/impl/temp.ts new file mode 100644 index 0000000..6ffcce9 --- /dev/null +++ b/src/commands/impl/temp.ts @@ -0,0 +1,41 @@ +import type { ChatMessage } from "@twurple/chat"; +import { getTemperatures } from "../../util/api-homeassistant.ts"; +import { logSuccess } from "../../util/logger.ts"; +import { BaseCommand } from "../base-command.ts"; +import type { ICommandRequirements } from "../interface.ts"; + +export class TempCommand extends BaseCommand { + name = "temp"; + cooldown = 0; + enabled = true; + + requirements: ICommandRequirements = { + developer: false, + mod: false, + }; + + triggered = async ( + channel: string, + user: string, + _text: string, + msg: ChatMessage, + ) => { + logSuccess(`${channel} ${user} temp command triggered`); + const temp = await getTemp(); + this.chatClient.say(channel, `it is ${temp}°C in darius room right now`, { + replyTo: msg, + }); + }; +} + +async function getTemp(): Promise { + const entities = await getTemperatures(); + const values = entities + .map((entity) => parseFloat(entity.state)) + .filter((value) => !Number.isNaN(value)); + const average = + values.length > 0 + ? values.reduce((sum, value) => sum + value, 0) / values.length + : 0; + return average.toFixed(2); +} diff --git a/src/commands/impl/volume.ts b/src/commands/impl/volume.ts new file mode 100644 index 0000000..e7ceef2 --- /dev/null +++ b/src/commands/impl/volume.ts @@ -0,0 +1,72 @@ +import type { ChatMessage } from "@twurple/chat"; +import { getVolumeFromTidal, setVolumeToTidal } from "../../util/api-tidal.ts"; +import { logSuccess } from "../../util/logger.ts"; +import { BaseCommand } from "../base-command.ts"; +import type { ICommandRequirements } from "../interface.ts"; + +export class VolumeCommand extends BaseCommand { + name = "vol"; + cooldown = 0; + enabled = true; + + requirements: ICommandRequirements = { + developer: true, + mod: false, + }; + + triggered = async ( + channel: string, + user: string, + text: string, + msg: ChatMessage, + ) => { + logSuccess(`${channel} ${user} volume command triggered`); + + const volumeText = await parseCommand(text); + if (volumeText) { + this.chatClient.say(channel, volumeText, { replyTo: msg }); + } else { + this.chatClient.say(channel, "tidal not running..", { replyTo: msg }); + } + }; +} + +async function parseCommand(message: string): Promise { + const args = message.slice(4).trim(); // Remove '!volume' + + // Case 1: no args + if (args === "") { + const volume = await getVolumeFromTidal(); + if (volume) { + return `volume is at ${volume.volume} right now`; + } + } + + const value = parseInt(args, 10); + + // Case 2: relative + const adjustMatch = args.match(/^([+-])(\d+)$/); + if (adjustMatch) { + const volume = await getVolumeFromTidal(); + if (volume) { + const wantedVolume = volume.volume + value; + const clampWantedVolume = clamp(wantedVolume); + await setVolumeToTidal(clampWantedVolume); + return `volume was at ${volume.volume} and is now ${clampWantedVolume}`; + } + } + + // Case 3: absolute + const setMatch = args.match(/^(\d+)$/); + if (setMatch) { + const clampValue = clamp(value); + await setVolumeToTidal(clampValue); + return `set volume to ${clampValue}`; + } + + return null; +} + +function clamp(value: number): number { + return Math.min(Math.max(value, 0), 100); +} diff --git a/src/commands/impl/where.ts b/src/commands/impl/where.ts new file mode 100644 index 0000000..421b94f --- /dev/null +++ b/src/commands/impl/where.ts @@ -0,0 +1,27 @@ +import type { ChatMessage } from "@twurple/chat"; +import { logSuccess } from "../../util/logger.ts"; +import { BaseCommand } from "../base-command.ts"; +import type { ICommandRequirements } from "../interface.ts"; + +export class WhereCommand extends BaseCommand { + name = "where"; + cooldown = 0; + enabled = true; + + requirements: ICommandRequirements = { + developer: false, + mod: false, + }; + + triggered = async ( + channel: string, + user: string, + _text: string, + msg: ChatMessage, + ) => { + logSuccess(`${channel} ${user} where command triggered`); + this.chatClient.say(channel, `leck eier`, { + replyTo: msg, + }); + }; +} diff --git a/src/config/config.ts b/src/config/config.ts index 84e56d0..9cb62a1 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -14,4 +14,12 @@ export const Config = { host: process.env.TIDAL_HOST || "", port: process.env.TIDAL_PORT || "", }, + + homeassistant: { + api_url: process.env.HA_API_URL || "", + api_token: process.env.HA_API_TOKEN || "", + + id_desk_sensor: process.env.HA_DESK_SENSOR_ID || "", + id_room_sensors: process.env.HA_ROOMTEMP_SENSOR_IDS?.split(",") || [], + }, } as const; diff --git a/src/util/api-functions.ts b/src/util/api-functions.ts index fa4e656..87d867e 100644 --- a/src/util/api-functions.ts +++ b/src/util/api-functions.ts @@ -4,6 +4,35 @@ import { apiClient } from "../core/api-client.ts"; import { getUserId } from "./general.ts"; import { logError } from "./logger.ts"; +export async function banByIds( + channelId: UserIdResolvable, + userId: UserIdResolvable, + reason: string, +): Promise { + if (!channelId || !userId) { + logError(`ban id command missing a required parameter`); + } + + await apiClient.asUser(Config.bot_user_id, async (apiClient) => + apiClient.moderation.banUser(channelId, { user: userId, reason }), + ); +} + +export async function ban( + channelName: string, + userName: string, + reason: string, +): Promise { + if (!channelName || !userName) { + logError(`ban name command missing a required parameter`); + } + + const channelId = await getUserId(channelName); + const userId = await getUserId(userName); + + await banByIds(channelId, userId, reason); +} + export async function timeoutByIds( channelId: UserIdResolvable, userId: UserIdResolvable, diff --git a/src/util/api-homeassistant.ts b/src/util/api-homeassistant.ts new file mode 100644 index 0000000..dd493eb --- /dev/null +++ b/src/util/api-homeassistant.ts @@ -0,0 +1,66 @@ +import axios from "axios"; +import { Config } from "../config/config.ts"; +import { logWarning } from "./logger.ts"; + +type HomeAssistantEntity = { + entity_id: string; + state: string; + attributes: { + state_class?: string; + unit_of_measurement?: string; + icon?: string; + friendly_name?: string; + [key: string]: unknown; + }; + last_changed: string; // datetime string + last_reported: string; // datetime string + last_updated: string; // datetime string + context: { + id: string; + parent_id: string | null; + user_id: string | null; + }; +}; + +export async function getDeskHeight(): Promise { + try { + return await sendRequestToHomeAssistant( + Config.homeassistant.id_desk_sensor, + ); + } catch { + logWarning("error getting hoehe from homeassistant"); + return null; + } +} + +export async function getTemperatures(): Promise> { + try { + const sensors = Config.homeassistant.id_room_sensors; + const results = []; + + for (const sensor_id of sensors) { + const res = await sendRequestToHomeAssistant(sensor_id); + results.push(res); + } + + return results; + } catch { + logWarning("error getting temperature from homeassistant"); + return []; + } +} + +async function sendRequestToHomeAssistant( + entity_id: string, +): Promise { + const response = await axios.get( + Config.homeassistant.api_url + entity_id, + { + headers: { + Authorization: `Bearer ${Config.homeassistant.api_token}`, + }, + }, + ); + + return response.data; +} diff --git a/src/util/api-tidal.ts b/src/util/api-tidal.ts new file mode 100644 index 0000000..1e5d75c --- /dev/null +++ b/src/util/api-tidal.ts @@ -0,0 +1,81 @@ +import axios from "axios"; +import { Config } from "../config/config.ts"; +import { logWarning } from "./logger.ts"; + +type Song = { + title: string; + artists: string; + album: string; + playingFrom: string; + status: "playing" | "paused"; + url: string; + current: string; + duration: string; +}; + +type Volume = { + volume: number; +}; + +export async function getSongFromTidalFormatted(): Promise { + const song = await getSongFromTidal(); + if (song) { + const status = song.status === "playing" ? "▶️" : "⏸️"; + return `listening to ${song.title} by ${song.artists}. ${status} ${song.current}/${song.duration}. link: ${song.url}`; + } else { + return `no song found`; + } +} + +async function getSongFromTidal(): Promise { + try { + const response = await sendGetRequestToTidal("current"); + + return response; + } catch { + logWarning("error getting song from tidal"); + return null; + } +} + +export async function getVolumeFromTidal(): Promise { + try { + const response = await sendGetRequestToTidal("volume"); + + return response; + } catch { + logWarning("error getting volume from tidal"); + return null; + } +} + +export async function setVolumeToTidal(volume: number): Promise { + try { + const response = await sendPostRequestToTidal("volume", { volume }); + + return response; + } catch { + logWarning("error setting volume from tidal"); + return null; + } +} + +async function sendGetRequestToTidal(endpoint: string): Promise { + const response = await axios.get( + `${Config.tidal.host}:${Config.tidal.port}/${endpoint}`, + ); + + return response.data; +} + +async function sendPostRequestToTidal( + endpoint: string, + data: unknown, +): Promise { + const response = await axios.post( + `${Config.tidal.host}:${Config.tidal.port}/${endpoint}`, + data, + ); + + return response.data; +}