Skip to content

Commit

Permalink
feat author button
Browse files Browse the repository at this point in the history
  • Loading branch information
oddyamill committed Sep 2, 2024
1 parent f79bdf2 commit 04cd8fd
Show file tree
Hide file tree
Showing 16 changed files with 395 additions and 91 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,8 @@ dist

.dev.vars
.wrangler/

# ide

.idea
.vscode
264 changes: 186 additions & 78 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
"devDependencies": {
"@cloudflare/workers-types": "^4.20240117.0",
"typescript": "^5.0.4",
"wrangler": "^3.0.0"
"wrangler": "^3.73.0"
},
"dependencies": {
"@oddyamill/discord-workers": "^1.1.2",
"discord-api-types": "^0.37.92"
"@oddyamill/discord-workers": "^1.1.5",
"discord-api-types": "^0.37.98"
}
}
8 changes: 6 additions & 2 deletions src/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Env } from './env'
import { Interaction } from './interaction'
import {
getTikTok,
makeAuthorComponent,
makeComponent,
makeMessageURL,
resolveArguments,
Expand Down Expand Up @@ -39,7 +40,7 @@ export const command = async (interaction: Interaction, t: Translator, env: Env,
})
}

const { format, stream, url } = data
const { format, author, stream, url } = data

if (!stream) {
return editResponse(interaction, {
Expand All @@ -58,7 +59,10 @@ export const command = async (interaction: Interaction, t: Translator, env: Env,
components: [
{
type: ComponentType.ActionRow,
components: [makeComponent(t('open_in_tiktok'), url)],
components: [
makeComponent(t('open_in_tiktok'), url),
await makeAuthorComponent(author.nickname, author.id, author.uniqueId, author.avatarThumb, interaction, env, ctx),
],
},
],
}
Expand Down
4 changes: 4 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export const TIKTOK_ENDPOINT = 'https://tiktok.com/@/video/'

export const TIKTOK_USER_ENDPOINT = 'https://tiktok.com/@'

export const MAX_FILE_LENGTH = 25 * 1024 * 1024

// https://stackoverflow.com/questions/74077377/regular-expression-to-match-any-tiktok-video-id-and-url#comment130792938_74077377
Expand All @@ -16,3 +18,5 @@ export const TIKTOK_HEADERS = {
}

export const IMAGE_WORKER_CACHE_TTL = 31556952

export const EMOJI_CACHE_TTL = 604800
3 changes: 2 additions & 1 deletion src/env.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { BotEnv } from '@oddyamill/discord-workers'

export interface Env extends BotEnv {
DISCORD_TOKEN: string
DISCORD_APPLICATION_ID: string
IMAGE_WORKER_ENDPOINT?: string
IMAGE_WORKER_CLIENT_ID: string
IMAGE_WORKER_CLIENT_SECRET: string
CACHE: KVNamespace
}
6 changes: 6 additions & 0 deletions src/tiktok.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,10 @@ export type TikTok = {
bitrateInfo?: { PlayAddr: { UrlList: [string] }; DataSize: number }[]
}
imagePost?: { images: [{ imageURL: { urlList: string } }] }
author: {
id: string
nickname: string
uniqueId: string
avatarThumb: string
}
}
54 changes: 54 additions & 0 deletions src/utils/arrayBufferToBase64.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
https://gist.github.com/958841
MIT LICENSE
Copyright 2011 Jon Leighton
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

const encodings =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

export const arrayBufferToBase64 = (buffer: ArrayBuffer) => {
const bytes = new Uint8Array(buffer),
byteLength = bytes.byteLength,
byteRemainder = byteLength % 3,
mainLength = byteLength - byteRemainder

let a, b, c, d, chunk, result = ''

for (let i = 0; i < mainLength; i = i + 3) {
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]

a = (chunk & 16515072) >> 18
b = (chunk & 258048) >> 12
c = (chunk & 4032) >> 6
d = chunk & 63

result += encodings[a] + encodings[b] + encodings[c] + encodings[d]
}

switch (byteRemainder) {
case 1:
chunk = bytes[mainLength]

a = (chunk & 252) >> 2
b = (chunk & 3) << 4

result += encodings[a] + encodings[b] + '=='
break

case 2:
chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]

a = (chunk & 64512) >> 10
b = (chunk & 1008) >> 4
c = (chunk & 15) << 2

result += encodings[a] + encodings[b] + encodings[c] + '='
break
}

return result
}
25 changes: 25 additions & 0 deletions src/utils/emojiCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { APIMessageComponentEmoji } from 'discord-api-types/v10'
import { EMOJI_CACHE_TTL } from '../constants'
import { Env } from '../env'

export const getComponentEmojiFromCache = async (
env: Env,
authorId: string
): Promise<APIMessageComponentEmoji | undefined> => {
return (await env.CACHE.get(authorId, 'json')) ?? undefined
}

export const putComponentEmojiToCache = async (
env: Env,
authorId: string,
emoji: APIMessageComponentEmoji
): Promise<void> => {
await env.CACHE.put(authorId, JSON.stringify(emoji), { expirationTtl: EMOJI_CACHE_TTL })
}

export const deleteComponentEmojiFromCache = async (
env: Env,
authorId: string
): Promise<void> => {
await env.CACHE.delete(authorId)
}
5 changes: 3 additions & 2 deletions src/utils/getTikTok.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ export const getTikTok = async (id: string, env: Env) => {

return {
url,
author: tiktok.author,
...await resolveMedia(response, tiktok, env),
}
} catch {
return
} catch (error) {
console.error(error)
}
}
2 changes: 2 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export * from './emojiCache'
export * from './getTikTok'
export * from './makeAuthorComponent'
export * from './makeComponent'
export * from './makeMessageURL'
export * from './resolveArguments'
Expand Down
60 changes: 60 additions & 0 deletions src/utils/makeAuthorComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { APIButtonComponentWithURL } from 'discord-api-types/v10'
import { makeComponent } from './makeComponent'
import { Env } from '../env'
import { Interaction } from '../interaction'
import { createApplicationEmoji } from '@oddyamill/discord-workers'
import { arrayBufferToBase64 } from './arrayBufferToBase64'
import { getComponentEmojiFromCache, putComponentEmojiToCache } from './emojiCache'
import { TIKTOK_USER_ENDPOINT } from '../constants'

export const makeAuthorComponent = async (
label: string,
authorId: string,
authorUsername: string,
authorAvatar: string,
interaction: Interaction,
env: Env,
ctx: ExecutionContext,
): Promise<APIButtonComponentWithURL> => {
const component = makeComponent(
label,
TIKTOK_USER_ENDPOINT + authorUsername,
await getComponentEmojiFromCache(env, authorId)
)

if (component.emoji !== undefined) {
return component
}

const url =
env.IMAGE_WORKER_ENDPOINT !== undefined
? env.IMAGE_WORKER_ENDPOINT + '/circle?url=' + encodeURIComponent(authorAvatar)
: authorAvatar

const image = await fetch(url, {
headers: {
'Cf-Access-Client-Id': env.IMAGE_WORKER_CLIENT_ID,
'Cf-Access-Client-Secret': env.IMAGE_WORKER_CLIENT_SECRET,
},
})

if (!image.ok) {
return component
}

try {
const emoji = await createApplicationEmoji(
env,
interaction.application_id,
authorId,
'data:image/png;base64,' + arrayBufferToBase64(await image.arrayBuffer())
)

component.emoji = { id: emoji.id!, name: emoji.name! }
ctx.waitUntil(putComponentEmojiToCache(env, authorId, component.emoji))
} catch (error) {
console.error(error)
}

return component
}
4 changes: 3 additions & 1 deletion src/utils/makeComponent.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import {
APIButtonComponentWithURL,
APIMessageComponentEmoji,
ButtonStyle,
ComponentType,
} from 'discord-api-types/v10'

export const makeComponent = (label: string, url: string): APIButtonComponentWithURL => {
export const makeComponent = (label: string, url: string, emoji?: APIMessageComponentEmoji): APIButtonComponentWithURL => {
return {
type: ComponentType.Button,
style: ButtonStyle.Link,
label,
url,
emoji,
}
}
11 changes: 8 additions & 3 deletions src/utils/resolveMedia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import {
import { resolveCookie } from './resolveCookie'
import { Env } from '../env'

export const resolveMedia = async (response: Response, tiktok: TikTok, env: Env) => {
export interface Media {
stream: Response
format: string
}

export const resolveMedia = async (response: Response, tiktok: TikTok, env: Env): Promise<Media | {}> => {
const init: RequestInit = {
headers: {
Cookie: resolveCookie(response),
Expand All @@ -26,7 +31,7 @@ export const resolveMedia = async (response: Response, tiktok: TikTok, env: Env)
return {}
}

const resolveImage = async (tiktok: TikTok, init: RequestInit, env: Env) => {
const resolveImage = async (tiktok: TikTok, init: RequestInit, env: Env): Promise<Media> => {
const { imagePost } = tiktok

if (imagePost!.images.length > 1 && env.IMAGE_WORKER_ENDPOINT !== undefined) {
Expand Down Expand Up @@ -58,7 +63,7 @@ const resolveImage = async (tiktok: TikTok, init: RequestInit, env: Env) => {
}
}

const resolveVideo = async (tiktok: TikTok, init: RequestInit) => {
const resolveVideo = async (tiktok: TikTok, init: RequestInit): Promise<Media | {}> => {
for (const bitrateInfo of tiktok.video.bitrateInfo!) {
if (bitrateInfo.DataSize > MAX_FILE_LENGTH) {
continue
Expand Down
21 changes: 20 additions & 1 deletion src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import {
InteractionResponseType,
InteractionType,
} from 'discord-api-types/v10'
import { getInteraction, respond } from '@oddyamill/discord-workers'
import { deleteApplicationEmoji, getApplicationEmojis, getInteraction, respond } from '@oddyamill/discord-workers'
import { Interaction } from './interaction'
import { Env } from './env'
import { getTranslator } from './localization'
import { command } from './command'
import { deleteComponentEmojiFromCache } from './utils'

export default {
async fetch(request, env, ctx): Promise<Response> {
Expand All @@ -25,4 +26,22 @@ export default {

return command(interaction, getTranslator(interaction.locale), env, ctx)
},
async scheduled(_, env, ctx): Promise<void> {
const { items: emojis } = await getApplicationEmojis(env, env.DISCORD_APPLICATION_ID)

if (emojis.length === 0) {
return
}

const promises = []

for (const emoji of emojis) {
promises.push(
deleteApplicationEmoji(env, env.DISCORD_APPLICATION_ID, emoji.id!),
deleteComponentEmojiFromCache(env, emoji.name!)
)
}

ctx.waitUntil(Promise.allSettled(promises))
},
} satisfies ExportedHandler<Env>
8 changes: 8 additions & 0 deletions wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,11 @@ main = "src/worker.ts"
compatibility_date = "2024-01-17"
keep_vars = true
workers_dev = false
upload_source_maps = true

[[kv_namespaces]]
binding = "CACHE"
id = "da05bb0c7ad34bfdac1f061d5c7d2717"

[triggers]
crons = ["0 12 * * 1"]

0 comments on commit 04cd8fd

Please sign in to comment.