├── .gitignore ├── src ├── db │ ├── __init__.py │ └── db.py ├── config │ ├── __init__.py │ └── config.py ├── setup │ ├── __init__.py │ └── setup.py ├── discordbot │ ├── __init__.py │ └── discordbot.py └── main.py ├── Makefile ├── images └── intents.png ├── settings.json.example ├── LICENSE.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | __pycache__ -------------------------------------------------------------------------------- /src/db/__init__.py: -------------------------------------------------------------------------------- 1 | __title__ = "db" 2 | __version__ = "0.0.1" 3 | 4 | from .db import connect -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | mkdir -p /etc/dgc/ 3 | cp -n settings.json.example /etc/dgc/settings.json -------------------------------------------------------------------------------- /images/intents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamemann/Discord-Global-Chat/HEAD/images/intents.png -------------------------------------------------------------------------------- /src/config/__init__.py: -------------------------------------------------------------------------------- 1 | __title__ = "config" 2 | __version__ = "0.0.1" 3 | 4 | from .config import getconfig -------------------------------------------------------------------------------- /src/setup/__init__.py: -------------------------------------------------------------------------------- 1 | __title__ = "setup" 2 | __version__ = "0.0.1" 3 | 4 | from .setup import setuptables -------------------------------------------------------------------------------- /src/discordbot/__init__.py: -------------------------------------------------------------------------------- 1 | __title__ = "discordbot" 2 | __version__ = "0.0.1" 3 | 4 | from .discordbot import connect -------------------------------------------------------------------------------- /src/db/db.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | def connect(location): 4 | conn = sqlite3.connect(location) 5 | conn.row_factory = sqlite3.Row 6 | 7 | return conn -------------------------------------------------------------------------------- /settings.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "BotToken": "", 3 | "BotMsgStayTime": 10.0, 4 | "UpdateTime": 60.0, 5 | "AppendGuildName": true, 6 | "AllowMentions": false 7 | } -------------------------------------------------------------------------------- /src/setup/setup.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | import sys 3 | import sqlite3 4 | 5 | def setuptables(conn): 6 | cur = conn.cursor() 7 | 8 | cur.execute("CREATE TABLE IF NOT EXISTS `channels` (guildid integer PRIMARY KEY, channelid integer, webhookurl text)") 9 | conn.commit() 10 | 11 | print("[SETUP] Created all tables.") -------------------------------------------------------------------------------- /src/config/config.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | def getconfig(cfgfile): 4 | cfg = {} 5 | 6 | with open(cfgfile) as f: 7 | cfg = json.load(f) 8 | 9 | # Set defaults if need to be. 10 | if 'BotMsgStayTime' not in cfg: 11 | cfg['BotMsgStayTime'] = 10.0 12 | 13 | if 'UpdateTime' not in cfg: 14 | cfg['UpdateTime'] = 60.0 15 | 16 | if 'AppendGuildName' not in cfg: 17 | cfg['AppendGuildName'] = True 18 | 19 | if 'AllowMentions' not in cfg: 20 | cfg['AllowMentions'] = False 21 | 22 | return cfg -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import config 4 | import discordbot 5 | import db 6 | import setup 7 | 8 | def main(): 9 | # Set default values for CFG file and SQLite databse locations. 10 | cfgfile = "/etc/dgc/settings.json" 11 | sqlitedb = "/etc/dgc/dgc.db" 12 | 13 | # Loop through all arguments. 14 | for arg in sys.argv: 15 | # Handle config file. 16 | if arg.startswith("cfg="): 17 | cfgfile = arg.split('=')[1] 18 | elif arg.startswith("sqlite="): 19 | sqlitedb = arg.split('=')[1] 20 | 21 | # Now connect to SQLite. 22 | conn = db.connect(sqlitedb) 23 | 24 | # Attempt to setup tables if they aren't already along with get information. 25 | setup.setuptables(conn) 26 | 27 | # Get config from JSON file. 28 | cfg = config.getconfig(cfgfile) 29 | 30 | # Connect Discord bot. 31 | discordbot.connect(cfg, conn) 32 | 33 | if __name__ == "__main__": 34 | main() -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Christian Deacon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discord Global Chat 2 | ## Description 3 | A Discord bot that allows for global chat between Discord servers in certain channels. Used for the Unnamed Discord community brand. 4 | 5 | ## Requirements 6 | The Discord.py [package](https://pypi.org/project/discord.py/) is required in order to use this bot. You may install this via the following command. 7 | 8 | ``` 9 | python3 -m pip install -U discord.py 10 | ``` 11 | 12 | ### Enable Intents 13 | You must enable intents within the Discord Developers portal under your project's "Bot" tab. You should only need "Server Members Intent", but I've enabled all three just in-case. 14 | 15 | ![Intents Image](./images/intents.png) 16 | 17 | ## Command Line Usage 18 | You may specify the settings JSON (used for the bot token, etc) and the SQLite DB location within the command line. The default settings location is `/etc/dgc/settings.json` and the default SQLite DB location is `/etc/dgc/dgc.db`. 19 | 20 | The following are examples of how to set these in the program. 21 | 22 | ``` 23 | python3 src/main.py cfg=/home/cdeacon/settings.json sqlite=/home/cdeacon/dgc.db 24 | ``` 25 | 26 | ## Config 27 | The config file is in JSON format and the following keys are supported. 28 | 29 | * **BotToken** - The Discord bot token. Please retrieve this from the Discord Developers page for your bot. 30 | * **BotMsgStayTime** - When the bot replies to a command in a text channel, delete the bot message this many seconds after (default - **10.0** seconds). 31 | * **UpdateTime** - How often to update the channel and web hook URL cache in seconds (default - **60.0** seconds). 32 | 33 | ## Bot Commands 34 | The command prefix is `!`. You must execute these commands inside of a text channel of the guild you want to modify. You must also be an administrator in order to use these commands. 35 | 36 | ### dgc_linkchannel 37 | ``` 38 | !dgc_linkchannel 39 | ``` 40 | 41 | Adds a channel to the linked global chat. If the channel ID is left blank, it will choose the channel the message was sent in. 42 | 43 | ### dgc_unlinkchannel 44 | ``` 45 | !dgc_unlinkchannel 46 | ``` 47 | 48 | Unlinks a channel to the linked global chat. 49 | 50 | ### dgc_updatehook 51 | ``` 52 | !dgc_updatehook 53 | ``` 54 | 55 | Updates the web hook that messages send to within the current guild. This must be pointed towards the correct channel ID set with `dgc_linkchannel`. 56 | 57 | ## Installing 58 | You may use `make install` within this directory to create the `/etc/dgc/` directory and copy `settings.json.example` to `/etc/dgc/settings.json`. Please configure the `settings.json` file to your needs. 59 | 60 | Other than that, the needed SQLite tables are created if they don't exist when the Python program is started. However, if need to be, here is the current table structure. 61 | 62 | ```SQL 63 | CREATE TABLE IF NOT EXISTS `channels` (guildid integer PRIMARY KEY, channelid integer, webhookurl text) 64 | ``` 65 | 66 | ## Starting 67 | As of right now, you'll want to use `python3` against the `src/main.py` file. Something like the following should work. 68 | 69 | ```bash 70 | python3 src/main.py 71 | ``` 72 | 73 | If there's a better way to handle this, please let me know. 74 | 75 | ## Credits 76 | * [Christian Deacon](https://github.com/gamemann) -------------------------------------------------------------------------------- /src/discordbot/discordbot.py: -------------------------------------------------------------------------------- 1 | import os 2 | import base64 3 | import time 4 | import aiohttp 5 | import aiohttp 6 | 7 | import discord 8 | from discord.ext import commands, tasks 9 | from discord.ext.commands import has_permissions, MissingPermissions 10 | from discord import Webhook 11 | from discord import Intents 12 | from discord.errors import NotFound 13 | 14 | import db 15 | 16 | bot = commands.Bot(command_prefix='!', intents=Intents.all()) 17 | channels = {} 18 | webhooks = {} 19 | 20 | def connect(cfg, conn): 21 | # Get connection cursor. 22 | cur = conn.cursor() 23 | 24 | @bot.event 25 | async def on_ready(): 26 | print("Successfully connected to Discord.") 27 | await updateinfo() 28 | 29 | @bot.command(name="dgc_linkchannel") 30 | @has_permissions(administrator=True) 31 | async def dgc_linkchannel(ctx, id=None): 32 | chnlid = 0 33 | chnl = 0 34 | 35 | if id is not None: 36 | chnlid = id 37 | else: 38 | chnlid = ctx.channel.id 39 | 40 | try: 41 | chnl = await bot.fetch_channel(chnlid) 42 | except NotFound: 43 | await ctx.channel.send("**Error** - Could not find channel with ID **" + chnlid + "** in current Discord guild.", delete_after=cfg['BotMsgStayTime']) 44 | 45 | return 46 | 47 | newchannels = [chnlid] 48 | cur = conn.cursor() 49 | 50 | # Retrieve current channel list if any. 51 | cur.execute("SELECT `channelid` FROM `channels` WHERE `guildid`=?", [ctx.guild.id]) 52 | conn.commit() 53 | 54 | exist = cur.fetchone() 55 | 56 | if exist is not None and len(exist) > 0: 57 | cur.execute("UPDATE `channels` SET `channelid`=? WHERE `guildid`=?", (chnlid, ctx.guild.id)) 58 | conn.commit() 59 | else: 60 | cur.execute("INSERT INTO `channels` (`guildid`, `channelid`, `webhookurl`) VALUES (?, ?, '')", (ctx.guild.id, chnlid)) 61 | conn.commit() 62 | 63 | await updateinfo() 64 | 65 | await ctx.channel.send("Successfully linked channel!", delete_after=cfg['BotMsgStayTime']) 66 | 67 | @bot.command(name="dgc_unlinkchannel") 68 | @has_permissions(administrator=True) 69 | async def dgc_unlinkchannel(ctx, name=None): 70 | chnlid = 0 71 | chnl = 0 72 | 73 | if id is not None: 74 | chnlid = id 75 | else: 76 | chnlid = ctx.channel.id 77 | 78 | try: 79 | chnl = await bot.fetch_channel(chnlid) 80 | except NotFound: 81 | await ctx.channel.send("**Error** - Could not find channel with ID **" + chnlid + "** in current Discord guild. However, deleting from database anyways.", delete_after=cfg['BotMsgStayTime']) 82 | 83 | cur = conn.cursor() 84 | 85 | # Retrieve current channel list if any. 86 | cur.execute("SELECT `channelid` FROM `channels` WHERE `guildid`=?", [ctx.guild.id]) 87 | conn.commit() 88 | 89 | exist = cur.fetchone() 90 | 91 | if exist is None or len(exist) < 1: 92 | await ctx.channel.send("No results came back for specific guild. Channel must not exist.", delete_after=cfg['BotMsgStayTime']) 93 | 94 | return 95 | 96 | cur.execute("UPDATE `channels` SET `channelid`=0 WHERE `guildid`=?", [ctx.guild.id]) 97 | conn.commit() 98 | 99 | await updateinfo() 100 | 101 | await ctx.channel.send("Successfully unlinked channel!", delete_after=cfg['BotMsgStayTime']) 102 | 103 | @bot.command(name="dgc_updatehook") 104 | @has_permissions(administrator=True) 105 | async def dgc_updatehook(ctx, url=None): 106 | if url is None: 107 | ctx.channel.send("**Error** - You're missing the URL argument.", delete_after=cfg['BotMsgStayTime']) 108 | return 109 | 110 | cur = conn.cursor() 111 | 112 | cur.execute("SELECT `webhookurl` FROM `channels` WHERE `guildid`=?", [ctx.guild.id]) 113 | conn.commit() 114 | 115 | row = cur.fetchone() 116 | 117 | if row is None or len(row) < 1: 118 | # Insert. 119 | cur.execute("INSERT INTO `channels` (`guildid`, `channelid`, `webhookurl`) VALUES (?, 0, ?)", (ctx.guild.id, url)) 120 | conn.commit() 121 | else: 122 | # Update. 123 | cur.execute("UPDATE `channels` SET `webhookurl`=? WHERE `guildid`=?", (url, ctx.guild.id)) 124 | conn.commit() 125 | 126 | await updateinfo() 127 | await ctx.channel.send("Successfully updated Web Hook URL if row existed.", delete_after=cfg['BotMsgStayTime']) 128 | 129 | @bot.command(name="dgc_gethook") 130 | @has_permissions(administrator=True) 131 | async def dgc_gethook(ctx): 132 | cur = conn.cursor() 133 | 134 | cur.execute("SELECT `webhookurl` FROM `channels` WHERE `guildid`=?", [ctx.guild.id]) 135 | conn.commit() 136 | 137 | row = cur.fetchone() 138 | 139 | if row is None or len(row) < 1: 140 | await ctx.channel.send("Could not retrieve hook.", delete_after=cfg['BotMsgStayTime']) 141 | 142 | await ctx.channel.send("Web hook URL => " + str(row['webhookurl'])) 143 | 144 | @bot.event 145 | async def on_message(msg): 146 | # Make sure the user isn't the bot or a bot. 147 | if msg.author.id == bot.user.id or msg.author.bot == True: 148 | return 149 | 150 | # If this is a webhook, ignore. 151 | if msg.webhook_id != None: 152 | return 153 | 154 | chnlid = msg.channel.id 155 | 156 | # Check to see if this is a global channel. 157 | if channels is None or msg.guild.id not in channels or msg.channel.id != channels[msg.guild.id]: 158 | await bot.process_commands(msg) 159 | 160 | return 161 | 162 | # Loop through all cached channels. 163 | for guild, chnl in channels.items(): 164 | # Ignore if this is the current channel. 165 | if chnl is None or (msg.guild.id == guild and chnl == chnlid): 166 | continue 167 | 168 | # Try to fetch the channel by ID. 169 | try: 170 | chnlobj = await bot.fetch_channel(chnl) 171 | except NotFound: 172 | channels[guild].remove(chnl) 173 | 174 | continue 175 | 176 | # Get guild name. 177 | guildname = False 178 | 179 | try: 180 | guildobj = await bot.fetch_guild(msg.guild.id) 181 | guildname = guildobj.name 182 | except (Forbidden, HTTPException) as e: 183 | guildname = False 184 | 185 | msgtosend = msg.content 186 | 187 | ## Append guild name to message. 188 | if guildname != False and cfg['AppendGuildName']: 189 | msgtosend = msgtosend + "\n\n*From " + guildname + "*" 190 | 191 | # Now send to the Discord channel. 192 | async with aiohttp.ClientSession() as session: 193 | webhook = Webhook.from_url(webhooks[guild], session=session) 194 | 195 | # Check for mentions. 196 | mentions = discord.AllowedMentions(everyone=False, users=False, roles=False, replied_user=False) 197 | 198 | if cfg['AllowMentions']: 199 | mentions = discord.AllowedMentions(everyone=False, users=True, roles=False, replied_user=False) 200 | 201 | await webhook.send(msgtosend, username=msg.author.display_name, avatar_url=msg.author.avatar_url, allowed_mentions=mentions) 202 | 203 | await bot.process_commands(msg) 204 | 205 | @tasks.loop(seconds=cfg['UpdateTime']) 206 | async def updateinfo(): 207 | print("Updating channels and web hook URLs...") 208 | cur = conn.cursor() 209 | 210 | for guild in bot.guilds: 211 | # Perform SQL query to retrieve all channels for this specific guild. 212 | cur.execute("SELECT `channelid`, `webhookurl` FROM `channels` WHERE `guildid`=?", [guild.id]) 213 | conn.commit() 214 | 215 | # Reset channels list and web hook URL. 216 | channels[guild.id] = None 217 | webhooks[guild.id] = None 218 | 219 | row = cur.fetchone() 220 | 221 | if row is None or len(row) < 1: 222 | #print("Couldn't fetch guild #" + str(guild.id)) 223 | continue 224 | 225 | # Assign web hook. 226 | webhooks[guild.id] = row['webhookurl'] 227 | 228 | # Assign channel ID. 229 | channels[guild.id] = row['channelid'] 230 | 231 | #print("Updated #" + str(guild.id) + " to:\n\nWeb hook => " + str(webhooks[guild.id]) + "\nChannel ID => " + str(channels[guild.id]) + "\n\n") 232 | 233 | bot.run(cfg['BotToken']) --------------------------------------------------------------------------------