├── .gitignore ├── docs ├── CNAME ├── images │ └── timesheriff.png ├── css │ ├── root.css │ ├── header.css │ ├── footer.css │ ├── style.css │ └── main.css └── index.html ├── package.json ├── LICENSE ├── commands ├── about.js ├── help.js ├── deleteinfo.js ├── gettime.js ├── settimezone.js └── timezones.js ├── .github └── FUNDING.yml ├── registerCommands.js ├── models └── User.js ├── README.md ├── embeds └── aboutBotEmbed.js ├── PRIVACY.md ├── TERMS.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .env 3 | node_modules 4 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | timesheriff.xyz 2 | www.timesheriff.xyz 3 | -------------------------------------------------------------------------------- /docs/images/timesheriff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xikodev/timesheriff/HEAD/docs/images/timesheriff.png -------------------------------------------------------------------------------- /docs/css/root.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-background-color: #3A6B89; 3 | --secondary-background-color: #000; 4 | 5 | --primary-color: #FFF; 6 | --secondary-color: #d9ad75; 7 | 8 | --secondary-color-hover: #7111D8; 9 | --tertiary-color-hover: #45C6D7; 10 | 11 | --discord: #5865F2; 12 | } 13 | -------------------------------------------------------------------------------- /docs/css/header.css: -------------------------------------------------------------------------------- 1 | @import 'root.css'; 2 | 3 | header { 4 | background-color: var(--primary-background-color); 5 | width: calc(100% - 2em); 6 | padding: 1em; 7 | display: flex; 8 | flex-wrap: wrap; 9 | align-items: center; 10 | } 11 | 12 | header img { 13 | width: 100px; 14 | height: 100px; 15 | } 16 | -------------------------------------------------------------------------------- /docs/css/footer.css: -------------------------------------------------------------------------------- 1 | @import 'root.css'; 2 | 3 | footer { 4 | width: calc(100% - 2em); 5 | background-color: var(--secondary-background-color); 6 | padding: 1em; 7 | display: flex; 8 | justify-content: space-between; 9 | flex-wrap: wrap; 10 | align-items: center; 11 | margin-top: auto; 12 | } 13 | 14 | footer a { 15 | color: var(--primary-color); 16 | margin: 0 0.5em; 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "timesheriff", 3 | "version": "1.0.0", 4 | "description": "Discord bot for timezones", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Borna Krpan", 10 | "license": "ISC", 11 | "dependencies": { 12 | "discord.js": "^14.21.0", 13 | "dotenv": "^17.2.0", 14 | "moment-timezone": "^0.6.0", 15 | "mysql2": "^3.14.3", 16 | "nodemon": "^3.1.10" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | All Rights Reserved 2 | 3 | Copyright (c) 2025 Borna Krpan (xikodev) 4 | 5 | This software and associated documentation files (the "Software") are the exclusive property of the copyright holder. 6 | 7 | Permission is NOT granted to use, copy, modify, merge, publish, distribute, sublicense, or sell copies of the Software. 8 | 9 | Any unauthorized use of the Software is strictly prohibited and may result in legal action. 10 | 11 | For inquiries regarding licensing or usage, contact: borna5krpan@gmail.com. 12 | -------------------------------------------------------------------------------- /commands/about.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder, MessageFlags } = require('discord.js'); 2 | const { aboutBotEmbed } = require('../embeds/aboutBotEmbed'); 3 | 4 | module.exports = { 5 | data: new SlashCommandBuilder() 6 | .setName('about') 7 | .setDescription('Info about the TimeSheriff bot'), 8 | 9 | async execute(interaction) { 10 | await interaction.reply({ 11 | embeds: [aboutBotEmbed], 12 | flags: MessageFlags.Ephemeral 13 | }); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /docs/css/style.css: -------------------------------------------------------------------------------- 1 | @import 'root.css'; 2 | 3 | html { 4 | height: 100vh; 5 | } 6 | 7 | body { 8 | margin: 0; 9 | background: linear-gradient(180deg, var(--primary-background-color) 20%, var(--secondary-background-color) 55%) fixed; 10 | color: var(--primary-color); 11 | font-family: Verdana, sans-serif; 12 | font-size: 1em; 13 | height: 100%; 14 | box-sizing: border-box; 15 | overflow-x: hidden; 16 | display: flex; 17 | flex-direction: column; 18 | align-items: stretch; 19 | } 20 | -------------------------------------------------------------------------------- /commands/help.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder, MessageFlags } = require('discord.js'); 2 | 3 | module.exports = { 4 | data: new SlashCommandBuilder() 5 | .setName('help') 6 | .setDescription('List all bot commands'), 7 | 8 | async execute(interaction) { 9 | const commandList = interaction.client.commands.map(cmd => { 10 | return `• \`/${cmd.data.name}\` — ${cmd.data.description}`; 11 | }).join('\n'); 12 | 13 | await interaction.reply({ 14 | content: `🛠️ **TimeSheriff Commands**\n\n${commandList}\n\nUse \`/settimezone\` first to configure your local time.`, 15 | flags: MessageFlags.Ephemeral 16 | }); 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: xikodev 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: xikodev 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /registerCommands.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const { REST, Routes } = require("discord.js"); 3 | const fs = require('fs'); 4 | 5 | const commands = []; 6 | const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js')); 7 | 8 | for (const file of commandFiles) { 9 | const command = require(`./commands/${file}`); 10 | commands.push(command.data.toJSON()); 11 | } 12 | 13 | const rest = new REST({ version: '10' }).setToken(process.env.TOKEN); 14 | 15 | (async () => { 16 | try { 17 | console.log('Registering / commands...'); 18 | 19 | await rest.put( 20 | Routes.applicationCommands(process.env.CLIEND_ID), 21 | { 22 | body: commands 23 | } 24 | ); 25 | 26 | console.log('Commands registered successfully.'); 27 | } catch (error) { 28 | console.error(error); 29 | } 30 | })(); 31 | -------------------------------------------------------------------------------- /commands/deleteinfo.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder, MessageFlags } = require('discord.js'); 2 | const User = require('../models/User'); 3 | 4 | module.exports = { 5 | data: new SlashCommandBuilder() 6 | .setName('deleteinfo') 7 | .setDescription('Delete your timezone information'), 8 | 9 | async execute(interaction) { 10 | const userId = interaction.user.id; 11 | 12 | try { 13 | await User.delete(userId); 14 | 15 | return interaction.reply({ 16 | content: `✅ Your information has been deleted.`, 17 | flags: MessageFlags.Ephemeral 18 | }); 19 | } catch (err) { 20 | console.error(err); 21 | return interaction.reply({ 22 | content: '⚠️ Failed to delete user information.', 23 | flags: MessageFlags.Ephemeral 24 | }); 25 | } 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const mysql = require('mysql2/promise'); 3 | 4 | const pool = mysql.createPool({ 5 | host: process.env.DB_HOST, 6 | user: process.env.DB_USER, 7 | password: process.env.DB_PASSWORD, 8 | database: process.env.DB_NAME, 9 | }); 10 | 11 | class User { 12 | constructor(userId, timezone) { 13 | this.userId = userId; 14 | this.timezone = timezone; 15 | } 16 | 17 | static async get(userId) { 18 | const sql = 'SELECT * FROM users WHERE userId = ? LIMIT 1'; 19 | const [rows] = await pool.execute(sql, [userId]); 20 | 21 | if (rows.length === 0) { 22 | return null; 23 | } else { 24 | return new User(rows[0].userId, rows[0].timezone); 25 | } 26 | } 27 | 28 | async save() { 29 | const sql = ` 30 | INSERT INTO users (userId, timezone) VALUES (?, ?) 31 | ON DUPLICATE KEY UPDATE timezone = ? 32 | `; 33 | 34 | await pool.execute(sql, [this.userId, this.timezone, this.timezone]); 35 | } 36 | 37 | static async delete(userId) { 38 | const sql = 'DELETE FROM users WHERE userId = ?'; 39 | const [result] = await pool.execute(sql, [userId]); 40 | 41 | if (result.affectedRows === 0) { 42 | throw new Error('User not found'); 43 | } 44 | } 45 | } 46 | 47 | module.exports = User; 48 | 49 | -------------------------------------------------------------------------------- /commands/gettime.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder, MessageFlags } = require('discord.js'); 2 | const moment = require('moment-timezone'); 3 | const User = require('../models/User'); 4 | 5 | module.exports = { 6 | data: new SlashCommandBuilder() 7 | .setName('gettime') 8 | .setDescription("Get a user's current local time") 9 | .addUserOption(option => 10 | option.setName('user') 11 | .setDescription('User to check timezone of') 12 | .setRequired(false)), 13 | 14 | async execute(interaction) { 15 | const user = interaction.options.getUser('user'); 16 | const targetId = user.id; 17 | let userData; 18 | 19 | if (targetId == null) { 20 | userData = await User.get(interaction.user.id); 21 | } else { 22 | userData = await User.get(targetId); 23 | } 24 | 25 | if (!userData) { 26 | return interaction.reply({ 27 | content: `❌ ${user.username} has not set their timezone yet.`, 28 | flags: MessageFlags.Ephemeral 29 | }); 30 | } 31 | 32 | const currentTime = moment().tz(userData.timezone).format('dddd, HH:mm [on] MMM D, YYYY'); 33 | return interaction.reply({ 34 | content: `${user.username}'s current local time is **${currentTime}** (${userData.timezone}).`, 35 | flags: MessageFlags.Ephemeral 36 | }); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TimeSheriff 2 | 3 | **TimeSheriff** is a lightweight, privacy-conscious Discord bot that helps users manage and view timezones — perfect for global communities, study groups, and gaming guilds. 4 | 5 | > ⚠️ All user data is private. No analytics, no tracking. Users can remove their data at any time using `/deleteinfo`. 6 | 7 | --- 8 | 9 | ## 🛠 Features 10 | 11 | - `/settimezone [timezone]` – Set your own timezone with autocomplete 12 | - `/gettime [user]` – View the local time of any user who has set their timezone 13 | - `/timezones [region]` – Browse available timezones (filterable by region: Europe, Asia, etc.) 14 | - `/deleteinfo` – Permanently delete your stored timezone 15 | - `/about` – Learn more about the bot 16 | - `/help` – Get a list of all commands 17 | 18 | --- 19 | 20 | ## 🚀 Getting Started 21 | 22 | To add TimeSheriff to your server, [invite the bot](https://discord.com/oauth2/authorize?client_id=1396929073107832933) with appropriate permissions (use `applications.commands` and `bot` scopes). 23 | 24 | --- 25 | 26 | ## 🔐 Privacy & Terms 27 | 28 | - [Privacy Policy](./PRIVACY.md) 29 | - [Terms of Service](./TERMS.md) 30 | - License: **All Rights Reserved** – You may not copy, modify, or redistribute any part of this software. See [LICENSE](./LICENSE). 31 | 32 | --- 33 | 34 | ## 📦 Tech Stack 35 | 36 | - [discord.js](https://discord.js.org/) for bot interactions 37 | - [MySQL](https://www.mysql.com/) for secure data storage 38 | - [moment-timezone](https://momentjs.com/timezone/) for timezone logic 39 | -------------------------------------------------------------------------------- /embeds/aboutBotEmbed.js: -------------------------------------------------------------------------------- 1 | 2 | const aboutBotEmbed = { 3 | color: 0x5555FF, 4 | title: 'About Bot', 5 | author: { 6 | name: 'xiko', 7 | icon_url: 'https://avatars.githubusercontent.com/u/58877412', 8 | url: 'https://github.com/xikodev', 9 | }, 10 | description: 'TimeSheriff helps your community stay in sync across the globe. It lets users set their timezone, view each other’s local times, and coordinate events effortlessly - no more mental math or missed pings!', 11 | thumbnail: { 12 | url: 'https://cdn.discordapp.com/app-icons/1396929073107832933/e2217f35d7599890b47519777141c818.png', 13 | }, 14 | fields: [ 15 | { 16 | name: 'Add to your server', 17 | value: '[🔗 Invite Link](https://discord.com/oauth2/authorize?client_id=1396929073107832933)', 18 | inline: true, 19 | }, 20 | { 21 | name: 'Official server', 22 | value: '[🔗 Server Link](https://discord.gg/VEnG7xER4v)', 23 | inline: true, 24 | }, 25 | { 26 | name: 'GitHub Repository', 27 | value: '[🔗 Github Link](https://github.com/xikodev/timesheriff)', 28 | inline: true, 29 | }, 30 | { 31 | name: '\u200b', 32 | value: '\u200b', 33 | inline: false, 34 | }, 35 | { 36 | name: '', 37 | value: '[Terms of Services](https://github.com/xikodev/timesheriff/blob/main/TERMS.md)\n' + 38 | '[Privacy policy](https://github.com/xikodev/timesheriff/blob/main/PRIVACY.md)', 39 | inline: true, 40 | }, 41 | ], 42 | footer: { 43 | text: 'TimeSheriff', 44 | icon_url: 'https://cdn.discordapp.com/app-icons/1396929073107832933/e2217f35d7599890b47519777141c818.png', 45 | }, 46 | timestamp: new Date().toISOString(), 47 | }; 48 | 49 | module.exports = { aboutBotEmbed }; 50 | -------------------------------------------------------------------------------- /commands/settimezone.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder, MessageFlags } = require('discord.js'); 2 | const moment = require('moment-timezone'); 3 | const User = require('../models/User'); 4 | 5 | const timezones = moment.tz.names(); 6 | 7 | module.exports = { 8 | data: new SlashCommandBuilder() 9 | .setName('settimezone') 10 | .setDescription('Set your timezone') 11 | .addStringOption(option => 12 | option.setName('timezone') 13 | .setDescription('IANA timezone (e.g. Europe/Zagreb)') 14 | .setAutocomplete(true) 15 | .setRequired(true) 16 | ), 17 | 18 | async execute(interaction) { 19 | let tz = interaction.options.getString('timezone'); 20 | const userId = interaction.user.id; 21 | 22 | if (!moment.tz.zone(tz)) { 23 | return interaction.reply({ content: '❌ Invalid timezone.', ephemeral: true }); 24 | } 25 | 26 | if (tz.includes('/')) { 27 | tz = tz 28 | .split('/') 29 | .map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()) 30 | .join('/'); 31 | } else { 32 | tz = tz.toUpperCase(); 33 | } 34 | 35 | 36 | try { 37 | await new User(userId, tz).save(); 38 | 39 | return interaction.reply({ 40 | content: `✅ Your timezone has been set to **${tz}**.`, 41 | flags: MessageFlags.Ephemeral 42 | }); 43 | } catch (err) { 44 | console.error(err); 45 | return interaction.reply({ 46 | content: '⚠️ Failed to save timezone.', 47 | flags: MessageFlags.Ephemeral 48 | }); 49 | } 50 | }, 51 | 52 | async autocomplete(interaction) { 53 | const focused = interaction.options.getFocused(); 54 | const matches = timezones 55 | .filter(tz => tz.toLowerCase().includes(focused.toLowerCase())) 56 | .slice(0, 25) 57 | .map(tz => ({ name: tz, value: tz })); 58 | 59 | await interaction.respond(matches); 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy for TimeSheriff Discord Bot 2 | 3 | Effective Date: 2025-07-22 4 | Last Updated: 2025-07-22 5 | 6 | Thank you for using TimeSheriff! This Privacy Policy explains how we collect, use, and protect your data when interacting with the TimeSheriff Discord bot. 7 | 8 | --- 9 | 10 | ## 1. What Information We Collect 11 | 12 | TimeSheriff collects and stores the following information: 13 | 14 | - Your Discord User ID 15 | - Your selected timezone (via the `/settimezone` command) 16 | 17 | We do **not** collect messages, email addresses, IP addresses, or any other personal or sensitive data. 18 | 19 | --- 20 | 21 | ## 2. How We Use Your Data 22 | 23 | We use the collected data solely to: 24 | 25 | - Display your local time when you or others run bot commands like `/gettime` 26 | - Provide time-based features to enhance Discord server interactions 27 | 28 | Your data is not used for analytics, marketing, profiling, or any external processing. 29 | 30 | --- 31 | 32 | ## 3. Data Sharing and Disclosure 33 | 34 | We **do not** share, sell, or transfer your data to third parties. All data is used strictly for the bot's core features. 35 | 36 | --- 37 | 38 | ## 4. Data Storage and Security 39 | 40 | All information is securely stored in a private MySQL database. Access to the database is restricted and protected using authentication credentials. 41 | 42 | Reasonable security measures are in place to prevent unauthorized access, alteration, or destruction of data. 43 | 44 | --- 45 | 46 | ## 5. Data Retention and Deletion 47 | 48 | You can delete your stored data at any time by using the `/deleteinfo` command in any server where the bot is active. 49 | 50 | Once executed, this command permanently removes your Discord ID and timezone from the database. 51 | 52 | --- 53 | 54 | ## 6. Your Rights 55 | 56 | - **View Your Data**: Use `/gettime` to check what timezone is currently set for your user. 57 | - **Update Your Data**: Use `/settimezone` to change your timezone. 58 | - **Delete Your Data**: Use `/deleteinfo` to permanently remove your stored information. 59 | 60 | --- 61 | 62 | ## 7. Changes to This Policy 63 | 64 | We may occasionally update this policy. Material changes will be announced via the bot (if possible) or documented in the GitHub repository. 65 | 66 | --- 67 | 68 | ## 8. Contact 69 | 70 | For privacy-related inquiries, data deletion assistance, or questions about this policy, contact: 71 | 72 | **TimeSheriff** 73 | Email: borna5krpan@gmail.com 74 | GitHub: https://github.com/xikodev/timesheriff 75 | -------------------------------------------------------------------------------- /TERMS.md: -------------------------------------------------------------------------------- 1 | # Terms of Service for TimeSheriff Discord Bot 2 | 3 | Effective Date: 2025-07-22 4 | 5 | Welcome to TimeSheriff! By using this Discord bot, you agree to the following terms of service (“Terms”). If you do not agree with these Terms, please do not use the bot. 6 | 7 | --- 8 | 9 | ## 1. Description 10 | 11 | TimeSheriff is a Discord bot that allows users to set and retrieve timezones, helping communities coordinate time-based interactions more easily. The bot operates within Discord servers and is accessed through Discord's slash command interface. 12 | 13 | --- 14 | 15 | ## 2. Eligibility 16 | 17 | By using TimeSheriff, you confirm that you are: 18 | - At least 13 years old or the minimum age required by Discord's own Terms of Service. 19 | - In compliance with all applicable local laws. 20 | 21 | --- 22 | 23 | ## 3. Data Collection and Privacy 24 | 25 | We collect only the minimum data necessary to operate the bot: 26 | - Your Discord User ID 27 | - Your selected timezone 28 | 29 | Your data is **never sold, shared, or monetized** in any way. 30 | For more information, please review our [Privacy Policy](PRIVACY.md). 31 | 32 | --- 33 | 34 | ## 4. Acceptable Use 35 | 36 | You agree **not to**: 37 | - Abuse or spam the bot 38 | - Attempt to reverse-engineer, exploit, or harm the bot or its services 39 | - Use the bot to harass, threaten, or impersonate others 40 | 41 | We reserve the right to restrict access to the bot at any time for violations of these terms. 42 | 43 | --- 44 | 45 | ## 5. Availability and Modifications 46 | 47 | TimeSheriff is provided “as is” with no guarantees of uptime, reliability, or continued feature support. 48 | We may update, remove, or add features at any time without notice. 49 | 50 | --- 51 | 52 | ## 6. Termination 53 | 54 | We reserve the right to: 55 | - Remove the bot from servers at our discretion 56 | - Ban or block users for violating these Terms 57 | - Discontinue the bot entirely without prior notice 58 | 59 | --- 60 | 61 | ## 7. Disclaimer 62 | 63 | We provide the bot **without any warranties**, express or implied. 64 | We are **not responsible** for any damages, losses, or issues arising from your use of the bot. 65 | 66 | --- 67 | 68 | ## 8. Governing Law 69 | 70 | These Terms shall be governed by and construed in accordance with the laws applicable in your country or region, without regard to its conflict of law provisions. 71 | 72 | --- 73 | 74 | ## 9. Contact 75 | 76 | If you have any questions about these Terms, please contact: 77 | 78 | **Email**: borna5krpan@gmail.com 79 | **GitHub**: https://github.com/xikodev/timesheriff 80 | -------------------------------------------------------------------------------- /docs/css/main.css: -------------------------------------------------------------------------------- 1 | @import 'root.css'; 2 | 3 | main { 4 | margin: 3em 0; 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: space-between; 8 | align-items: center; 9 | } 10 | 11 | section { 12 | width: 100%; 13 | padding: 3em 0; 14 | margin-bottom: 2em; 15 | display: flex; 16 | flex-direction: column; 17 | justify-content: space-around; 18 | align-items: center; 19 | } 20 | 21 | section.about-me { 22 | flex-direction: row; 23 | justify-content: flex-start; 24 | flex-wrap: wrap; 25 | } 26 | 27 | section.about-me article { 28 | flex-direction: row; 29 | } 30 | 31 | h1 { 32 | font-size: 2em; 33 | } 34 | 35 | section article { 36 | margin: 1em 10%; 37 | display: flex; 38 | flex-direction: column; 39 | justify-content: space-around; 40 | align-items: center; 41 | } 42 | 43 | section img { 44 | width: 300px; 45 | height: 300px; 46 | margin-right: 1em; 47 | } 48 | 49 | section p { 50 | width: 60%; 51 | } 52 | 53 | section a { 54 | padding: 0.5em; 55 | margin: 0.5em; 56 | text-decoration: none; 57 | border-radius: 1em; 58 | display: flex; 59 | align-items: center; 60 | transition: 0.3s; 61 | } 62 | 63 | main i { 64 | font-size: 2em; 65 | margin-right: 0.5em; 66 | } 67 | 68 | table td { 69 | padding: 0.5em 1em; 70 | } 71 | 72 | span.secondary { 73 | color: var(--secondary-color); 74 | } 75 | 76 | span.code { 77 | color: #ccc; 78 | background-color: #666; 79 | padding: 0.25em; 80 | border-radius: 0.25em; 81 | } 82 | 83 | a.discord { 84 | background-color: var(--discord); 85 | color: var(--primary-color); 86 | padding: 1em; 87 | margin: 0; 88 | width: fit-content; 89 | } 90 | 91 | @media (width <= 700px) { 92 | main { 93 | margin: 2em 3%; 94 | } 95 | 96 | header h2 { 97 | display: none; 98 | } 99 | 100 | section { 101 | width: 100%; 102 | padding: 1em 0; 103 | } 104 | 105 | hr { 106 | width: 95%; 107 | } 108 | 109 | section article { 110 | margin: 0 111 | } 112 | 113 | section.about-me article { 114 | flex-wrap: wrap; 115 | } 116 | 117 | section p { 118 | width: 90%; 119 | text-align: start; 120 | } 121 | 122 | section ul { 123 | padding: 0; 124 | } 125 | 126 | section ul li { 127 | flex-wrap: wrap; 128 | } 129 | 130 | section ul li h3 { 131 | margin: 0 1em; 132 | } 133 | 134 | div.links a { 135 | display: flex; 136 | flex-direction: column; 137 | justify-content: space-between; 138 | align-items: center; 139 | text-align: center; 140 | } 141 | 142 | div.links a i { 143 | margin: 0 0 0.25em; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /commands/timezones.js: -------------------------------------------------------------------------------- 1 | const { 2 | SlashCommandBuilder, 3 | ActionRowBuilder, 4 | ButtonBuilder, 5 | ButtonStyle, 6 | MessageFlags 7 | } = require('discord.js'); 8 | const moment = require('moment-timezone'); 9 | 10 | module.exports = { 11 | data: new SlashCommandBuilder() 12 | .setName('timezones') 13 | .setDescription('Browse valid timezones, optionally by region') 14 | .addStringOption(option => 15 | option.setName('region') 16 | .setDescription('Filter by region (e.g. Europe, Asia, America, etc.)') 17 | .setRequired(false) 18 | ), 19 | 20 | async execute(interaction) { 21 | const inputRegion = interaction.options.getString('region'); 22 | const allZones = moment.tz.names(); 23 | 24 | // Filter by region if provided 25 | const zones = inputRegion 26 | ? allZones.filter(z => z.toLowerCase().startsWith(inputRegion.toLowerCase() + '/')) 27 | : allZones; 28 | 29 | if (zones.length === 0) { 30 | return interaction.reply({ 31 | content: `❌ No timezones found for region: \`${inputRegion}\``, 32 | flags: MessageFlags.Ephemeral 33 | }); 34 | } 35 | 36 | const itemsPerPage = 20; 37 | const totalPages = Math.ceil(zones.length / itemsPerPage); 38 | 39 | const getPage = (page) => { 40 | const start = page * itemsPerPage; 41 | const end = start + itemsPerPage; 42 | const pageZones = zones.slice(start, end); 43 | return `🌍 **Timezones**${inputRegion ? ` in ${inputRegion}` : ''} (Page ${page + 1}/${totalPages})\n\n` + 44 | pageZones.map(z => `• ${z}`).join('\n'); 45 | }; 46 | 47 | let page = 0; 48 | 49 | const row = new ActionRowBuilder().addComponents( 50 | new ButtonBuilder() 51 | .setCustomId('prev') 52 | .setLabel('⬅️ Previous') 53 | .setStyle(ButtonStyle.Secondary) 54 | .setDisabled(true), 55 | new ButtonBuilder() 56 | .setCustomId('next') 57 | .setLabel('➡️ Next') 58 | .setStyle(ButtonStyle.Secondary) 59 | .setDisabled(totalPages <= 1) 60 | ); 61 | 62 | const reply = await interaction.reply({ 63 | content: getPage(page), 64 | components: [row], 65 | flags: MessageFlags.Ephemeral, 66 | fetchReply: true 67 | }); 68 | 69 | const collector = reply.createMessageComponentCollector({ 70 | time: 5 * 60 * 1000, // 5 minutes 71 | filter: i => i.user.id === interaction.user.id 72 | }); 73 | 74 | collector.on('collect', async i => { 75 | if (i.customId === 'prev' && page > 0) page--; 76 | else if (i.customId === 'next' && page < totalPages - 1) page++; 77 | 78 | const updatedRow = new ActionRowBuilder().addComponents( 79 | new ButtonBuilder() 80 | .setCustomId('prev') 81 | .setLabel('⬅️ Previous') 82 | .setStyle(ButtonStyle.Secondary) 83 | .setDisabled(page === 0), 84 | new ButtonBuilder() 85 | .setCustomId('next') 86 | .setLabel('➡️ Next') 87 | .setStyle(ButtonStyle.Secondary) 88 | .setDisabled(page === totalPages - 1) 89 | ); 90 | 91 | await i.update({ 92 | content: getPage(page), 93 | components: [updatedRow] 94 | }); 95 | }); 96 | 97 | collector.on('end', async () => { 98 | reply.edit({ components: [] }).catch(() => {}); 99 | }); 100 | } 101 | }; 102 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const { Client, Collection, IntentsBitField, ActivityType, MessageFlags } = require('discord.js'); 3 | const mysql = require('mysql2') 4 | const fs = require('fs'); 5 | 6 | 7 | // Creating the client object 8 | const client = new Client({ 9 | intents: [ 10 | IntentsBitField.Flags.Guilds, 11 | IntentsBitField.Flags.GuildMembers, 12 | IntentsBitField.Flags.GuildMessages, 13 | IntentsBitField.Flags.MessageContent, 14 | ], 15 | }); 16 | client.commands = new Collection(); 17 | 18 | 19 | // Load all slash commands 20 | const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js')); 21 | for (const file of commandFiles) { 22 | const command = require(`./commands/${file}`); 23 | client.commands.set(command.data.name, command); 24 | } 25 | 26 | 27 | // MySQL connection 28 | try { 29 | mysql.createConnection({ 30 | host: process.env.DB_HOST, 31 | user: process.env.DB_USER, 32 | password: process.env.DB_PASSWORD, 33 | database: process.env.DB_NAME, 34 | }); 35 | console.log('Connected to MySQL'); 36 | } catch(error) { 37 | console.error('MySQL connection error:', error); 38 | } 39 | 40 | 41 | // User interactions 42 | client.on('interactionCreate', async interaction => { 43 | if (interaction.user.bot) return; 44 | 45 | if (interaction.isChatInputCommand()) { 46 | const command = client.commands.get(interaction.commandName); 47 | if (!command) return; 48 | 49 | try { 50 | await command.execute(interaction); 51 | } catch (err) { 52 | console.error(err); 53 | await interaction.reply({ 54 | content: '❌ There was an error executing that command.', 55 | flags: MessageFlags.Ephemeral 56 | }); 57 | } 58 | } else if (interaction.isAutocomplete()) { 59 | const command = client.commands.get(interaction.commandName); 60 | if (!command || !command.autocomplete) return; 61 | 62 | try { 63 | await command.autocomplete(interaction); 64 | } catch (err) { 65 | console.error(err); 66 | } 67 | } 68 | }); 69 | 70 | 71 | // Send joined server info 72 | client.on('guildCreate', async (guild) => { 73 | const reportChannelId = '1397960988460060813'; 74 | 75 | try { 76 | const inviteChannel = guild.channels.cache.find( 77 | (ch) => 78 | ch.type === 0 && // Text channel 79 | ch.permissionsFor(guild.members.me).has(['CreateInstantInvite', 'ViewChannel']) 80 | ); 81 | 82 | if (!inviteChannel) { 83 | console.warn(`No valid channel to create invite in ${guild.name}`); 84 | return; 85 | } 86 | 87 | const invite = await inviteChannel.createInvite({ 88 | maxAge: 0, // Permanent 89 | maxUses: 0, // Unlimited 90 | reason: 'Reporting new server join to home server', 91 | }); 92 | 93 | const homeChannel = await client.channels.fetch(reportChannelId); 94 | 95 | if (!homeChannel || homeChannel.type !== 0) { 96 | console.warn('Report channel not found or invalid.'); 97 | return; 98 | } 99 | 100 | await homeChannel.send( 101 | `🛰️ I've just joined a new server: **${guild.name}**\n📨 Invite link: ${invite.url}` 102 | ); 103 | 104 | console.log(`Reported new server join: ${guild.name}`); 105 | } catch (error) { 106 | console.error('Error reporting guild join:', error); 107 | } 108 | }); 109 | 110 | 111 | // Activity status 112 | client.on('ready', () => { 113 | console.log('Bot ' + client.user.username + ' is ready to use.'); 114 | 115 | client.user.setActivity({ 116 | name: 'timesheriff.xyz | /help', 117 | type: ActivityType.Custom 118 | }); 119 | }); 120 | 121 | 122 | // Bot initialization 123 | client.login(process.env.TOKEN) 124 | .then(() => console.log('Bot ' + client.user.username + ' started successfully.')) 125 | .catch((error) => console.error(error)); 126 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | TimeSheriff 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 |
19 | Profile picture 20 |

TimeSheriff

21 |
22 |
23 |
24 |
25 |
26 | Bot picture 27 |
28 |
29 |

TimeSheriff

30 |

31 | TimeSheriff helps your community stay in sync across the globe. It 32 | lets users set their timezone, view each other’s local times, and coordinate events effortlessly — 33 | no more mental math or missed pings! 34 |

35 |

36 | TimeSheriff is a lightweight, privacy-conscious 37 | bot perfect for global communities, study groups, and gaming guilds. 38 |

39 | Add to your server 40 |
41 |
42 |
43 | 44 |
45 |

Features

46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 |
CommandsDescription
/settimezone [timezone]Set your own timezone with autocomplete
/gettime [user]View the local time of any user who has set their timezone
/timezones [region]Browse available timezones (filterable by region: Europe, Asia, etc.)
/deleteinfoPermanently delete your stored timezone
/aboutLearn more about the bot
/helpGet a list of all commands
81 |
82 |
83 |
84 | 92 | 93 | 94 | --------------------------------------------------------------------------------