├── .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 |
20 | TimeSheriff
21 |
22 |
23 |
24 |
25 |
26 |
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 | Commands
51 | Description
52 |
53 |
54 |
55 |
56 | /settimezone [timezone]
57 | Set your own timezone with autocomplete
58 |
59 |
60 | /gettime [user]
61 | View the local time of any user who has set their timezone
62 |
63 |
64 | /timezones [region]
65 | Browse available timezones (filterable by region: Europe, Asia, etc.)
66 |
67 |
68 | /deleteinfo
69 | Permanently delete your stored timezone
70 |
71 |
72 | /about
73 | Learn more about the bot
74 |
75 |
76 | /help
77 | Get a list of all commands
78 |
79 |
80 |
81 |
82 |
83 |
84 |
92 |
93 |
94 |
--------------------------------------------------------------------------------