Skip to content

Commit

Permalink
Renamed to Emote Cloner + added BetterTTV support
Browse files Browse the repository at this point in the history
  • Loading branch information
mxgic1337 committed Oct 21, 2023
1 parent f570fb0 commit 1f10754
Show file tree
Hide file tree
Showing 10 changed files with 213 additions and 59 deletions.
File renamed without changes
29 changes: 17 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
# 7TV Importer
**7TV Importer (Unofficial)** is a Discord bot that can import emotes from 7TV into Discord.
# Twitch Cloner
**Twitch Cloner** is a Discord bot that clones emotes from BetterTTV and 7TV into Discord.

7TV Importer is **not affiliated** with 7TV.
Twitch Cloner is **not affiliated** with Twitch, BetterTTV, FrankerFaceZ or 7TV.

[Add 7TV Importer to your server](https://discord.com/api/oauth2/authorize?client_id=1163079809719611413&permissions=8799314249792&scope=bot)
## 🔗 Links
- [🤖 Invite Link](https://discord.com/api/oauth2/authorize?client_id=1163079809719611413&permissions=8799314249792&scope=bot)

## Usage
To add an emote, use the **/emote** command.
## ✨ Features
- Cloning any emote from [BetterTTV](https://betterttv.com).
- Cloning any emote from [7TV](https://7tv.app).

![/emote Usage example](./gh_assets/example1.gif)
## ⚙️ Commands
- 🔧 **/emote** - Clones any emote from BTTV/7TV.
- 🔒 This command requires **Manage Expressions** permission.
- ⚙️ Parameters:
- 🔗 **url** (Required) - Emote URL (Supported platforms: BTTV, 7TV)
- 🔼 **size** - Emote size - 1x, 2x, 4x (3x for BTTV)
- 📝 **name** - Override default emote name
- 🛑 **disable_animations** - If animated, emote will be uploaded as static image

Command options:
- url (Required) - Emote URL
- size - Emote size (1x, 2x or 4x)
- name - Overwrite emote name
- disable_animations - If the emote is animated, it will be uploaded as non-animated image
![/emote Usage example](.github/example1.gif)
4 changes: 2 additions & 2 deletions bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {EmoteCommand} from "./commands/impl/EmoteCommand";

dotenv.config()

export const clientId = '' + process.env.CLIENT_ID;
export const token: string = '' + process.env.TOKEN
export const clientId = '' + (process.env.DEV_MODE === "true" ? process.env.DEV_CLIENT_ID : process.env.CLIENT_ID);
export const token: string = '' + (process.env.DEV_MODE === "true" ? process.env.DEV_TOKEN : process.env.TOKEN)
const client = new Client({ intents: [GatewayIntentBits.Guilds] });

client.on('ready', (c)=>{
Expand Down
5 changes: 2 additions & 3 deletions commands/commands.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
ApplicationCommandOptionType,
ChatInputCommandInteraction,
PermissionFlagsBits,
REST,
Expand All @@ -12,7 +11,7 @@ import {clientId, token} from "../bot";
const commandData = [
new SlashCommandBuilder()
.setName("emote")
.setDescription("Adds an 7TV emote.")
.setDescription("Adds an BetterTTV/7TV emote.")
.addStringOption(option =>
option.setName('url')
.setDescription("Emote URL")
Expand All @@ -23,7 +22,7 @@ const commandData = [
.addChoices(
{name: '1x', value: '1x'},
{name: '2x', value: '2x'},
{name: '4x', value: '4x'},
{name: '4x (3x on BTTV)', value: '4x'},
))
.addStringOption(option =>
option.setName('name')
Expand Down
105 changes: 81 additions & 24 deletions commands/impl/EmoteCommand.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {Command} from "../commands";
import {ChatInputCommandInteraction, EmbedBuilder} from "discord.js";
import {getEmoteDataFromURL, isURLValid} from "../../util/SevenTVUtil";
import {Emote, getEmoteDataFromURL as getBTTVEmote} from "../../util/BetterTTVUtil";
import {getEmoteDataFromURL as get7TVEmote} from "../../util/SevenTVUtil";
import {isEmoteURL} from "../../util/URLUtil";

export class EmoteCommand extends Command {
constructor() {
Expand All @@ -11,15 +13,50 @@ export class EmoteCommand extends Command {
const disableAnimationsOption = interaction.options.get('disable_animations');
if (urlOption === null) return;
const emoteURL: string = urlOption.value as string
if (isURLValid(emoteURL, 'emotes')) {
const emote = await getEmoteDataFromURL(emoteURL);
const name = nameOption !== null ? nameOption.value : emote.name
const platform = isEmoteURL(emoteURL);
if (platform !== undefined) {
let emote: Emote | undefined;
switch (platform) {
case '7tv':
emote = await get7TVEmote(emoteURL)
break
case 'bttv':
emote = await getBTTVEmote(emoteURL)
break
default:
emote = undefined
}
if (emote === undefined) return;
const name: string = nameOption !== null ? nameOption.value as string : emote.name
const disableAnimations = disableAnimationsOption !== null ? disableAnimationsOption.value as boolean : false

const animatedURL = emote.hostURL.replace('{{size}}', sizeOption !== null ? platform === 'bttv' && sizeOption.value === '4x' ? '3x' : '4x' : '1x') + '.gif';
const animatedFullURL = emote.hostURL.replace('{{size}}', platform === 'bttv' ? '3x' : '4x') + '.gif';
const staticURL = emote.hostURL.replace('{{size}}', sizeOption !== null ? platform === 'bttv' && sizeOption.value === '4x' ? '3x' : '4x' : platform === 'bttv' ? '3x' : '4x') + '.webp';
const staticFullURL = emote.hostURL.replace('{{size}}', platform === 'bttv' ? '3x' : '4x') + '.webp';

let platformIcon: string;
let platformText: string;
switch (emote.platform) {
case "bttv":
platformIcon = '<:BetterTTV:1165355805487399097>';
platformText = 'BetterTTV'
break;
case "ffz":
platformIcon = '<:FrankerFaceZ:1165355987096580097>';
platformText = 'FrankerFaceZ'
break;
case "7tv":
platformIcon = '<:7TV:1165355988841402458>';
platformText = '7TV'
break;
}

const embed = new EmbedBuilder()
.setTitle(`${emote.name} by ${emote.owner.display_name}`)
.setTitle(`${emote.name} by ${emote.author.name}`)
.setAuthor({
name: emote.owner.display_name,
iconURL: 'https:' + emote.owner.avatar_url
name: emote.author.name,
iconURL: emote.author.avatar
})
.setTimestamp()
.setFooter({
Expand All @@ -41,36 +78,45 @@ export class EmoteCommand extends Command {
},
{
name: 'Emote Author',
value: emote.owner.display_name,
value: emote.author.name,
inline: true,
},
{
name: 'Animated?',
value: emote.animated ? `Yes${disableAnimations ? ' (Disabled)' : ''}` : 'No',
inline: true,
},
{
name: 'Platform',
value: `${platformIcon} ${platformText}`,
inline: true,
},
])
.setThumbnail(emote.animated ? 'https:' + emote.host.url + `/4x.gif` : 'https:' + emote.host.url + `/4x.webp`)
interaction.reply({embeds: [embed]}).then(()=>{
.setThumbnail(emote.animated ? emote.hostURL.replace('{{size}}', sizeOption !== null ? platform === "bttv" && (sizeOption.value as string) === "4x" ? "3x" : sizeOption.value as string : '4x.gif') : emote.hostURL.replace('{{size}}', sizeOption !== null ? platform === "bttv" && (sizeOption.value as string) === "4x" ? "3x" : sizeOption.value as string : platform === 'bttv' ? '3x.webp' : '4x.webp'))
interaction.reply({embeds: [embed]}).then(() => {
if (!interaction.guild) return;
if (emote === undefined) return;
interaction.guild.emojis.create({
attachment: emote.animated && !disableAnimations ? 'https:' + emote.host.url + `/${sizeOption !== null ? sizeOption.value : '1x'}.gif` : 'https:' + emote.host.url + `/${sizeOption !== null ? sizeOption.value : '4x'}.webp`,
name: name
attachment: emote.animated && !disableAnimations ?
animatedURL :
staticURL,
name: name
}).then(emoji => {
console.error(`Uploaded emote in guild ${interaction.guildId}.`)
if (emote === undefined) return;
const embed = new EmbedBuilder()
.setTitle(`${emote.name} by ${emote.owner.display_name}`)
.setTitle(`${emote.name} by ${emote.author.name}`)
.setAuthor({
name: emote.owner.display_name,
iconURL: 'https:' + emote.owner.avatar_url
name: emote.author.name,
iconURL: emote.author.avatar
})
.setFooter({
text: 'Added by ' + interaction.user.username,
iconURL: interaction.user.avatarURL() !== null ? '' + interaction.user.avatarURL() : interaction.user.defaultAvatarURL
})
.setTimestamp()
.setColor('#00ff59')
.setDescription(`Successfully added <${emoji.animated ? 'a' : ''}:${emoji.name}:${emoji.id}> **${emote.name}${name !== emote.name ? ` (${name})` : ''}** emote to Discord\nSelected size: \`${sizeOption !== null ? sizeOption.value : emote.animated && !disableAnimations? '1x (Default)' : '4x (Default)'}\``)
.setDescription(`Successfully added <${emoji.animated ? 'a' : ''}:${emoji.name}:${emoji.id}> **${emote.name}${name !== emote.name ? ` (${name})` : ''}** emote to Discord\nSelected size: \`${sizeOption !== null ? sizeOption.value : emote.animated && !disableAnimations ? '1x (Default)' : '4x (Default)'}\``)
.setFields([
{
name: 'State',
Expand All @@ -84,24 +130,30 @@ export class EmoteCommand extends Command {
},
{
name: 'Emote Author',
value: emote.owner.display_name,
value: emote.author.name,
inline: true,
},
{
name: 'Animated?',
value: emote.animated ? `Yes${disableAnimations ? ' (Disabled)' : ''}` : 'No',
inline: true,
},
{
name: 'Platform',
value: `${platformIcon} ${platformText}`,
inline: true,
},
])
.setThumbnail(emote.animated ? 'https:' + emote.host.url + '/4x.gif' : 'https:' + emote.host.url + '/4x.webp')
.setThumbnail(emote.animated ? animatedFullURL : staticFullURL)
interaction.editReply({embeds: [embed]})
}).catch(err => {
console.error(`Emote upload in guild ${interaction.guildId} failed: ${err.message}`)
if (emote === undefined) return;
const embed = new EmbedBuilder()
.setTitle(`${emote.name} by ${emote.owner.display_name}`)
.setTitle(`${emote.name} by ${emote.author.name}`)
.setAuthor({
name: emote.owner.display_name,
iconURL: 'https:' + emote.owner.avatar_url
name: emote.author.name,
iconURL: emote.author.avatar
})
.setDescription(`Selected size: \`${sizeOption !== null ? sizeOption.value : emote.animated && !disableAnimations ? '1x (Default)' : '4x (Default)'}\``)
.setTimestamp()
Expand All @@ -128,16 +180,21 @@ export class EmoteCommand extends Command {
},
{
name: 'Emote Author',
value: emote.owner.display_name,
value: emote.author.name,
inline: true,
},
{
name: 'Animated?',
value: emote.animated ? `Yes${disableAnimations ? ' (Disabled)' : ''}` : 'No',
inline: true,
},
{
name: 'Platform',
value: `${platformIcon} ${platformText}`,
inline: true,
},
])
.setThumbnail(emote.animated ? 'https:' + emote.host.url + '/4x.gif' : 'https:' + emote.host.url + '/4x.webp')
.setThumbnail(emote.animated ? animatedFullURL : staticFullURL)
interaction.editReply({embeds: [embed]})
})
})
Expand All @@ -151,7 +208,7 @@ export class EmoteCommand extends Command {
})
.setTimestamp()
.setColor('#ff2020')
.setDescription("`❌` Invalid 7TV emote URL.")
.setDescription("`❌` Invalid emote URL.\nCurrently supported platforms: `BetterTTV, 7TV`")
console.log(interaction.user)
interaction.reply({embeds: [embed]}).then()
}
Expand Down
6 changes: 5 additions & 1 deletion example.env
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
CLIENT_ID=
TOKEN=
TOKEN=

DEV_MODE=false
DEV_CLIENT_ID=
DEV_TOKEN=
25 changes: 19 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
{
"name": "7tv-importer",
"version": "1.0.1",
"description": "Discord bot that imports 7TV emotes.",
"main": "index.js",
"name": "emote-cloner",
"version": "1.1.0",
"description": "Discord bot that clones BTTV/7TV emotes.",
"main": "dist/bot.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"dev": "ts-node-dev bot.ts",
"start": "node dist/bot.js",
"build": "tsc"
},
"homepage": "https://github.com/mxgic1337/emote-cloner",
"keywords": [],
"author": "mxgic1337_",
"license": "ISC",
"bugs": {
"url": "https://github.com/mxgic1337/emote-cloner/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/mxgic1337/emote-cloner"
},
"license": "GPL-3.0",
"dependencies": {
"discord.js": "^14.13.0",
"dotenv": "^16.3.1"
},
"devDependencies": {
"ts-node-dev": "^2.0.0"
}
}
37 changes: 37 additions & 0 deletions util/BetterTTVUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@


export async function getEmoteDataFromURL(url: string) {
return await getEmoteData(url.split('/')[4].split('/')[0])
}

export async function getEmoteData(id: string) {
const response = await fetch('https://api.betterttv.net/3/emotes/' + id)
if (response.ok) {
const emote = await response.json();
return {
platform: 'bttv',
name: emote.code,
hostURL: 'https://cdn.betterttv.net/emote/'+emote.id+'/{{size}}',
animated: emote.animated,
author: {
name: emote.user.displayName,
avatar: 'https://pbs.twimg.com/profile_images/1615415316657364994/r3yTCKWx_400x400.jpg',
}
} as Emote
}else{
const text = await response.text()
console.error(text)
return undefined
}
}

export type Emote = {
platform: 'bttv' | 'ffz' | '7tv',
name: string,
hostURL: string,
animated: boolean,
author: {
name: string,
avatar: string
}
}
24 changes: 13 additions & 11 deletions util/SevenTVUtil.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
export function isURLValid(url: string, urlType: 'emotes' | 'users' | 'emote-sets') {
switch (urlType) {
default:
return /^((http|https):\/\/)(www.|)7tv.app\/emotes\/[a-zA-Z0-9]{24}/gi.test(url)
case 'users':
return /^((http|https):\/\/)(www.|)7tv.app\/users\/[a-zA-Z0-9]{24}/gi.test(url)
case 'emote-sets':
return /^((http|https):\/\/)(www.|)7tv.app\/emote-sets\/[a-zA-Z0-9]{24}/gi.test(url)
}
}
import {Emote} from "./BetterTTVUtil";


export async function getEmoteDataFromURL(url: string) {
return await getEmoteData(url.split('/')[4].split('/')[0])
Expand All @@ -16,7 +8,17 @@ export async function getEmoteDataFromURL(url: string) {
export async function getEmoteData(id: string) {
const response = await fetch('https://7tv.io/v3/emotes/' + id)
if (response.ok) {
return await response.json()
const emote = await response.json();
return {
platform: '7tv',
name: emote.name,
hostURL: 'https:' + emote.host.url + '/{{size}}',
animated: emote.animated,
author: {
name: emote.owner.display_name,
avatar: 'https:' + emote.owner.avatar_url,
}
} as Emote
}else{
const text = await response.text()
console.error(text)
Expand Down
Loading

0 comments on commit 1f10754

Please sign in to comment.