The weeb for the next gen discord boat - Wamellow wamellow.com
bot discord
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

add twitch notifications

Luna 861b2f8e c9bb687b

+213 -71
+103
app/dashboard/[guildId]/notifications/create-twitch.component.tsx
··· 1 + "use client"; 2 + 3 + import Image from "next/image"; 4 + import { useState } from "react"; 5 + 6 + import { guildStore } from "@/common/guilds"; 7 + import DumbTextInput from "@/components/inputs/dumb-text-input"; 8 + import SelectMenu from "@/components/inputs/select-menu"; 9 + import Modal from "@/components/modal"; 10 + import TutorialPic from "@/public/docs-assets/notifications-channel-urls.webp"; 11 + import { ApiV1GuildsModulesNotificationsGetResponse, NotificationType } from "@/typings"; 12 + import { createSelectableItems } from "@/utils/create-selectable-items"; 13 + 14 + const URL_CHANNEL_REGEX = /^https?:\/\/((www|m)\.)?twitch\.tv\/([a-zA-Z0-9_-]{1,32})$/; 15 + const CHANNE_HANDLE = /^@?[a-zA-Z0-9._-]{1,32}$/; 16 + 17 + function validateAccount(input: string) { 18 + if (URL_CHANNEL_REGEX.exec(input)) return input.split(".tv/")[1]; 19 + if (CHANNE_HANDLE.exec(input)) return input.replace("@", ""); 20 + return null; 21 + } 22 + 23 + interface Props { 24 + add: (notification: ApiV1GuildsModulesNotificationsGetResponse) => void; 25 + set: (id: string) => void; 26 + 27 + isOpen: boolean; 28 + onClose: () => void; 29 + } 30 + 31 + export function TwitchNotificationModal({ 32 + add, 33 + set, 34 + 35 + isOpen, 36 + onClose 37 + }: Props) { 38 + const guildId = guildStore((g) => g?.id); 39 + const channels = guildStore((g) => g?.channels); 40 + 41 + const [name, setName] = useState(""); 42 + const [channelId, setChannelId] = useState<string | null>(null); 43 + 44 + return (<> 45 + <Modal<ApiV1GuildsModulesNotificationsGetResponse> 46 + title="Create new notification" 47 + isOpen={isOpen} 48 + onClose={onClose} 49 + onSubmit={() => { 50 + const validated = validateAccount(name); 51 + if (!validated && name.startsWith("https://")) return new Error("Invalid channel url"); 52 + if (!validated) return new Error("Invalid channel id or handle"); 53 + 54 + return fetch(`${process.env.NEXT_PUBLIC_API}/guilds/${guildId}/modules/notifications`, { 55 + method: "POST", 56 + credentials: "include", 57 + headers: { 58 + "Content-Type": "application/json" 59 + }, 60 + body: JSON.stringify({ 61 + type: NotificationType.Twitch, 62 + channelId, 63 + creatorHandle: validated 64 + }) 65 + }); 66 + }} 67 + onSuccess={(tag) => { 68 + add(tag); 69 + set(tag.id); 70 + 71 + setName(""); 72 + setChannelId(null); 73 + }} 74 + > 75 + <DumbTextInput 76 + name="Streamer's username or URL" 77 + placeholder="DarkViperAU" 78 + value={name} 79 + setValue={setName} 80 + /> 81 + 82 + <SelectMenu 83 + name="Channel" 84 + dataName="channelId" 85 + items={createSelectableItems(channels)} 86 + description="Select a channel where notifications should be send into." 87 + onSave={(o) => { 88 + setChannelId(o.value as string); 89 + }} 90 + /> 91 + 92 + <div> 93 + <span className="text-lg dark:text-neutral-300 text-neutral-700 font-medium">How to get a streamer&apos;s username</span> 94 + <Image 95 + alt="How to get a Creator's @handle, id or URL" 96 + className="rounded-md" 97 + src={TutorialPic} 98 + /> 99 + </div> 100 + 101 + </Modal> 102 + </>); 103 + }
+10 -17
app/dashboard/[guildId]/notifications/create-youtube.component.tsx
··· 1 1 "use client"; 2 2 3 3 import Image from "next/image"; 4 - import Link from "next/link"; 5 4 import { useState } from "react"; 6 5 7 6 import { guildStore } from "@/common/guilds"; 8 7 import DumbTextInput from "@/components/inputs/dumb-text-input"; 9 8 import SelectMenu from "@/components/inputs/select-menu"; 10 9 import Modal from "@/components/modal"; 11 - import TutorialPic from "@/public/docs-assets/notifications-get-handle.webp"; 12 - import { ApiV1GuildsModulesNotificationsGetResponse } from "@/typings"; 10 + import TutorialPic from "@/public/docs-assets/notifications-channel-urls.webp"; 11 + import { ApiV1GuildsModulesNotificationsGetResponse, NotificationType } from "@/typings"; 13 12 import { createSelectableItems } from "@/utils/create-selectable-items"; 14 13 15 - const URL_CHANNEL_REGEX = /^https:\/\/((www|m)\.)?youtube\.com\/channel\/UC([a-zA-Z0-9_-]{16,32})$/; 16 - const URL_HANDLE_REGEX = /^https:\/\/((www|m)\.)?youtube\.com\/@([a-zA-Z0-9._-]{3,30})$/; 14 + const URL_CHANNEL_REGEX = /^https?:\/\/((www|m)\.)?youtube\.com\/channel\/UC([a-zA-Z0-9_-]{16,32})$/; 15 + const URL_HANDLE_REGEX = /^https?:\/\/((www|m)\.)?youtube\.com\/@([a-zA-Z0-9._-]{3,30})$/; 17 16 const CHANNEL_ID = /^UC[a-zA-Z0-9_-]{16,32}$/; 18 17 const CHANNE_HANDLE = /^@?[a-zA-Z0-9._-]{3,30}$/; 19 18 ··· 67 66 "Content-Type": "application/json" 68 67 }, 69 68 body: JSON.stringify({ 70 - type: 0, 69 + type: NotificationType.YouTube, 71 70 channelId, 72 71 creatorHandle: isId ? undefined : validated, 73 72 creatorId: isId ? validated : undefined ··· 101 100 102 101 <div> 103 102 <span className="text-lg dark:text-neutral-300 text-neutral-700 font-medium">How to get a channel&apos;s @handle or Id</span> 104 - 105 - <Link 106 - href="/docs/notifications" 107 - target="_blank" 108 - > 109 - <Image 110 - alt="How to get a Creator's @handle, id or URL" 111 - className="rounded-md" 112 - src={TutorialPic} 113 - /> 114 - </Link> 103 + <Image 104 + alt="How to get a Creator's @handle, id or URL" 105 + className="rounded-md" 106 + src={TutorialPic} 107 + /> 115 108 </div> 116 109 117 110 </Modal>
+33 -9
app/dashboard/[guildId]/notifications/page.tsx
··· 2 2 3 3 import Image from "next/image"; 4 4 import { useParams } from "next/navigation"; 5 + import { BsTwitch, BsYoutube } from "react-icons/bs"; 5 6 import { HiChat, HiViewGridAdd } from "react-icons/hi"; 6 7 7 8 import { guildStore } from "@/common/guilds"; 8 9 import Fetch from "@/components/button-fetch"; 10 + import { ClientBadge } from "@/components/client"; 9 11 import { CreateSplash } from "@/components/dashboard/lists/create-splash"; 10 12 import { useList } from "@/components/dashboard/lists/hook"; 11 13 import { Navigation } from "@/components/dashboard/lists/navigation"; ··· 14 16 import SelectMenu from "@/components/inputs/select-menu"; 15 17 import { ScreenMessage } from "@/components/screen-message"; 16 18 import SadWumpusPic from "@/public/sad-wumpus.gif"; 17 - import { ApiV1GuildsModulesNotificationsGetResponse } from "@/typings"; 19 + import { ApiV1GuildsModulesNotificationsGetResponse, NotificationType } from "@/typings"; 20 + import { cn } from "@/utils/cn"; 18 21 import { createSelectableItems } from "@/utils/create-selectable-items"; 19 22 20 23 import { DeleteNotification } from "./delete.component"; ··· 84 87 const channel = guild?.channels?.find((channel) => channel.id === item.channelId); 85 88 86 89 return (<> 87 - <Image 88 - alt={`${item.creator.username}'s avatar`} 89 - className="rounded-full" 90 - src={item.creator.avatarUrl} 91 - width={46} 92 - height={46} 93 - /> 90 + <ClientBadge 91 + className="aspect-square bg-[#1c1b1f]" 92 + content={<Icon type={item.type} className="size-4 mt-0.5" />} 93 + showOutline={false} 94 + size="sm" 95 + placement="bottom-left" 96 + > 97 + <Image 98 + alt={`${item.creator.username}'s avatar`} 99 + className="rounded-full" 100 + src={item.creator.avatarUrl} 101 + width={46} 102 + height={46} 103 + /> 104 + </ClientBadge> 94 105 95 106 <div className="flex flex-col items-start"> 96 - <span className="text-neutral-100 text-lg font-medium -mb-[0.5]"> 107 + <span className="flex gap-2 text-neutral-100 text-lg font-medium -mb-[0.5]"> 97 108 {item.creator.username} 98 109 </span> 99 110 ··· 181 192 onSave={(value) => editItem("message", { content: value.content ?? null, embed: value.embed })} 182 193 /> 183 194 </>); 195 + } 196 + 197 + function Icon({ 198 + type, 199 + className 200 + }: { 201 + type: NotificationType; 202 + className?: string; 203 + }) { 204 + switch (type) { 205 + case NotificationType.YouTube: return <BsYoutube className={cn("text-red-500", className)} />; 206 + case NotificationType.Twitch: return <BsTwitch className={cn("text-violet-500", className)} />; 207 + } 184 208 }
+26 -19
app/dashboard/[guildId]/notifications/select.component.tsx
··· 1 + import { PopoverClose } from "@radix-ui/react-popover"; 2 + import React, { useEffect, useState } from "react"; 3 + import { BsTwitch, BsYoutube } from "react-icons/bs"; 4 + 5 + import { badgeVariants } from "@/components/ui/badge"; 1 6 import { Button } from "@/components/ui/button"; 7 + import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer"; 2 8 import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; 3 9 import { ApiV1GuildsModulesNotificationsGetResponse } from "@/typings"; 4 - import { BsTwitch, BsYoutube } from "react-icons/bs"; 10 + import { cn } from "@/utils/cn"; 11 + 12 + import { TwitchNotificationModal } from "./create-twitch.component"; 5 13 import { YoutubeNotificationModal } from "./create-youtube.component"; 6 - import React, { useEffect, useState } from "react"; 7 - import { PopoverClose } from "@radix-ui/react-popover"; 8 - import { TwitchNotificationModal } from "./create-twitch.component"; 9 - import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer"; 10 - import { badgeVariants } from "@/components/ui/badge"; 11 - import { cn } from "@/utils/cn"; 12 14 13 15 export const Style = { 14 16 Compact: 1, ··· 31 33 icon: <BsYoutube />, 32 34 name: Platform.YouTube 33 35 }, 34 - ] 36 + { 37 + icon: <BsTwitch />, 38 + name: Platform.Twitch 39 + } 40 + ]; 35 41 36 42 export function CreateNotificationSelect({ 37 43 style, ··· 41 47 const [isMobile, setIsMobile] = useState(false); 42 48 const [platform, setPlatform] = useState<Platform | null>(null); 43 49 44 - useEffect(() => { 45 - const mediaQuery = window.matchMedia("(max-width: 768px)"); // Adjust breakpoint as needed 46 - const handleMediaChange = () => setIsMobile(mediaQuery.matches); 50 + useEffect(() => { 51 + const mediaQuery = window.matchMedia("(max-width: 768px)"); // Adjust breakpoint as needed 52 + const handleMediaChange = () => setIsMobile(mediaQuery.matches); 47 53 48 - handleMediaChange(); // Check initial value 49 - mediaQuery.addEventListener("change", handleMediaChange); 54 + handleMediaChange(); // Check initial value 55 + mediaQuery.addEventListener("change", handleMediaChange); 50 56 51 - return () => mediaQuery.removeEventListener("change", handleMediaChange); 52 - }, []); 57 + return () => mediaQuery.removeEventListener("change", handleMediaChange); 58 + }, []); 53 59 54 60 const Wrapper = isMobile ? DrawerWrapper : PopoverWrapper; 55 61 56 62 return (<> 57 63 <Wrapper 58 64 button={ 59 - <Button 65 + <Button 60 66 className={style === Style.Compact ? cn(badgeVariants(), "h-6") : ""} 61 67 variant="secondary" 62 68 > ··· 76 82 /> 77 83 78 84 <YoutubeNotificationModal add={add} set={set} isOpen={platform === Platform.YouTube} onClose={() => setPlatform(null)} /> 79 - </>) 85 + <TwitchNotificationModal add={add} set={set} isOpen={platform === Platform.Twitch} onClose={() => setPlatform(null)} /> 86 + </>); 80 87 } 81 88 82 89 function PopoverWrapper({ ··· 84 91 button, 85 92 style 86 93 }: { 87 - children: (platform: typeof platforms[number]) => React.ReactNode; 94 + children: (platform: typeof platforms[number]) => React.ReactNode; 88 95 button: React.ReactNode; 89 96 style: typeof Style[keyof typeof Style]; 90 97 }) { ··· 140 147 ))} 141 148 </DrawerContent> 142 149 </Drawer> 143 - ) 150 + ); 144 151 }
+6
next.config.js
··· 41 41 hostname: "yt3.ggpht.com", 42 42 port: "", 43 43 pathname: "/**" 44 + }, 45 + { 46 + protocol: "https", 47 + hostname: "static-cdn.jtvnw.net", 48 + port: "", 49 + pathname: "/jtv_user_pictures/**" 44 50 } 45 51 ] 46 52 }
public/docs-assets/notifications-channel-urls.webp

This is a binary file and will not be displayed.

public/docs-assets/notifications-get-handle.webp

This is a binary file and will not be displayed.

+2 -2
public/docs/meta.json
··· 76 76 } 77 77 }, 78 78 { 79 - "name": "📢 YouTube Notifications", 79 + "name": "📢 Social Notifications", 80 80 "file": "notifications.md", 81 81 "image": "/docs-assets/open-graph/notifications-youtube.webp", 82 - "description": "Notify your community when a new YouTube video gets posted!", 82 + "description": "Notify your community when your favourite content creators post videos or go live!", 83 83 "permissions": { 84 84 "bot": [ 85 85 "View Channel",
+26 -22
public/docs/notifications.md
··· 1 - - We only support **YouTube**, use [NotifyMe](https://notifyme.bot) or [DisPing](https://disping.xyz) for Twitch, Kick, Twitter, etc. 2 - - Every server can have **up to 30 different channels for free**. 1 + - We only support **YouTube and Twitch**, use [NotifyMe](https://notifyme.bot) or [DisPing](https://disping.xyz) for TikTok, Kick, Twitter, etc. 2 + - Every server can have **up to 30 different channels and streamers for free**. 3 3 - Notifications are sent **within five to ten seconds** after uploading. 4 - - And **free custom message & embed** for every notification individually. 4 + - And **free custom messages** for every notification individually. 5 5 <br /> 6 6 7 7 <iframe src="https://www.youtube.com/embed/xizs-hrwK4I" height="513" frameborder="0" allow="autoplay"> ··· 11 11 1. Add Wamellow to your server by going to [wamellow.com/add](https://wamellow.com/add). 12 12 2. Head to the dashboard by going to [wamellow.com/dashboard](https://wamellow.com/dashboard?to=notifications). 13 13 3. Select your server from the dashboard. 14 - 4. Navigate to the **YouTube Notifications** tab. 15 - 5. Click **Add a YouTube channel** and enter a channel url/handle/id. 14 + 4. Navigate to the **Notifications** tab. 15 + 5. Click **Create new Notification**, select a platform and enter a channel url or @handle. 16 16 7. Click **Submit** and start customizing your message! 17 17 <br/> 18 18 19 - ![How to get YouTube channel handle or id](/docs-assets/notifications-get-handle.webp) 19 + ![Either copy the entire url, or the handle/id](/docs-assets/notifications-channel-urls.webp) 20 20 21 21 ### ✏️ Custom message & embed 22 22 You can create a notification message with a **fully customizable message and embed for free**, enabling you to style the messages the way you love. ··· 33 33 <br /> 34 34 <br /> 35 35 36 - **Note:** If Wamellow does not have the `Mention Everyone` permissions inside the channel, it might not be able to actually notify those roles. 36 + **Note:** If Wamellow does not have the `Mention Everyone` permissions inside the channel, it might not be able to actually notify members with those roles. 37 37 38 38 ## Placeholders 39 39 Placeholders allow you to use variables that change from message to message, for example to display information about the uploaded video or creator. They are always enclosed in curly braces, such as `{creator.name}`. 40 + <br /> 41 + <br /> 42 + 43 + **Note:** Placeholders marked with a <code>*</code> only work for YouTube. The examples assume you use YouTube, though they will work for any other platform. 40 44 41 45 <table> 42 46 <thead> 43 47 <tr> 44 - <th width="181">Placeholder</th> 48 + <th width="192">Placeholder</th> 45 49 <th>Example</th> 46 50 <th width="181">Description</th> 47 51 </tr> ··· 55 59 <tr> 56 60 <td><code>video.title</code></td> 57 61 <td>Your PC Can Look Like THIS Now!</td> 58 - <td>Video title</td> 62 + <td>Video/Stream title</td> 59 63 </tr> 60 64 <tr> 61 - <td><code>video.id</code></td> 65 + <td><code>video.id</code>*</td> 62 66 <td>74Lj5cHseI8</td> 63 67 <td>Video id</td> 64 68 </tr> 65 69 <tr> 66 70 <td><code>video.link</code></td> 67 - <td>https://www.youtube.com/watch?v=74Lj5cHseI8</td> 68 - <td>Video page</td> 71 + <td>https://youtube.com/watch?v=74Lj5cHseI8</td> 72 + <td>Video/Stream page</td> 69 73 </tr> 70 74 <tr> 71 75 <td><code>video.thumbnail</code></td> 72 76 <td>https://i4.ytimg.com/vi/74Lj5cHseI8/hqdefault.jpg</td> 73 - <td>Video thumbnail url</td> 77 + <td>Video/Stream image</td> 74 78 </tr> 75 79 <tr> 76 - <td><code>video.uploaded.ago</code></td> 80 + <td><code>video.uploaded.ago</code>*</td> 77 81 <td><t:1715878720:R></td> 78 82 <td>Time since upload</td> 79 83 </tr> 80 84 <tr> 81 - <td><code>video.uploaded.at</code></td> 85 + <td><code>video.uploaded.at</code>*</td> 82 86 <td><t:1715878720:f></td> 83 87 <td>Upload time & date</td> 84 88 </tr> ··· 88 92 <table> 89 93 <thead> 90 94 <tr> 91 - <th width="181">Placeholder</th> 95 + <th width="192">Placeholder</th> 92 96 <th>Example</th> 93 97 <th width="181">Description</th> 94 98 </tr> ··· 106 110 </tr> 107 111 <tr> 108 112 <td><code>creator.link</code></td> 109 - <td>https://www.youtube.com/@LinusTechTips</td> 113 + <td>https://youtube.com/@LinusTechTips</td> 110 114 <td>Creator page</td> 111 115 </tr> 112 116 <tr> 113 117 <td><code>creator.avatar</code></td> 114 - <td>https://yt3.ggpht.com/...</td> 118 + <td>https://.../...</td> 115 119 <td>Creator avatar url</td> 116 120 </tr> 117 121 <tr> 118 - <td><code>creator.subs</code></td> 122 + <td><code>creator.subs</code>*</td> 119 123 <td>16M</td> 120 124 <td>Subscriber count</td> 121 125 </tr> 122 126 <tr> 123 - <td><code>creator.videos</code></td> 127 + <td><code>creator.videos</code>*</td> 124 128 <td>6.9K</td> 125 129 <td>Amount of videos</td> 126 130 </tr> 127 131 <tr> 128 - <td><code>creator.views</code></td> 132 + <td><code>creator.views</code>*</td> 129 133 <td>7.8B</td> 130 134 <td>Total views</td> 131 135 </tr> ··· 135 139 <table> 136 140 <thead> 137 141 <tr> 138 - <th width="181">Placeholder</th> 142 + <th width="192">Placeholder</th> 139 143 <th>Example</th> 140 144 <th width="181">Description</th> 141 145 </tr>
+7 -2
typings.ts
··· 90 90 /** 91 91 * @description -1 represents the role being above the bot's highest role 92 92 */ 93 - permissions: -1 | 0; 93 + permissions: -1 | 0; 94 94 position: number; 95 95 color: number; 96 96 } ··· 402 402 }[]; 403 403 } 404 404 405 + export enum NotificationType { 406 + YouTube = 0, 407 + Twitch = 1 408 + } 409 + 405 410 export interface ApiV1GuildsModulesNotificationsGetResponse { 406 411 id: string; 407 412 guildId: string; 408 413 channelId: string; 409 414 roleId: string | null; 410 415 411 - type: 0; 416 + type: NotificationType; 412 417 creatorId: string; 413 418 414 419 message: {