├── .env ├── requirements.txt ├── assets ├── banner.png ├── header.png └── logo.png ├── manifest.json ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml ├── workflows │ └── docker-image.yml └── FUNDING.yml ├── Dockerfile ├── LICENSE ├── terms-of-service.md ├── privacy-policy.md ├── docs ├── script.js ├── index.html └── styles.css ├── README.md └── main.py /.env: -------------------------------------------------------------------------------- 1 | BOT_TOKEN = "YOUR_BOT_TOKEN" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | discord.py 2 | python-dotenv 3 | aiosqlite 4 | -------------------------------------------------------------------------------- /assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kenhendricks00/FixEmbed/HEAD/assets/banner.png -------------------------------------------------------------------------------- /assets/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kenhendricks00/FixEmbed/HEAD/assets/header.png -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kenhendricks00/FixEmbed/HEAD/assets/logo.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discordpy-basic", 3 | "version": "1.0.0", 4 | "type": "python" 5 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Discord 4 | url: https://discord.gg/QFxTAmtZdn 5 | about: You can ask general questions here. 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim-buster 2 | 3 | WORKDIR /app 4 | 5 | COPY requirements.txt requirements.txt 6 | RUN pip3 install -r requirements.txt 7 | 8 | COPY . . 9 | 10 | CMD ["python3", "main.py"] -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | # Build the Docker image 18 | - name: Build the Docker image 19 | run: docker build . --file Dockerfile --tag kenhendricks00/fixembed:latest 20 | 21 | # Log in to Docker Hub using credentials stored in GitHub Secrets 22 | - name: Log in to Docker Hub 23 | uses: docker/login-action@v2 24 | with: 25 | username: ${{ secrets.DOCKER_USERNAME }} 26 | password: ${{ secrets.DOCKER_PASSWORD }} 27 | 28 | # Push the Docker image to Docker Hub 29 | - name: Push the Docker image to Docker Hub 30 | run: docker push kenhendricks00/fixembed:latest 31 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: kenhendricks # 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: kenhendricks # Replace with a single Buy Me a Coffee username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Kenneth Hendricks 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 | -------------------------------------------------------------------------------- /terms-of-service.md: -------------------------------------------------------------------------------- 1 | # FixEmbed Terms of Service 2 | 3 | Last updated and effective: June 26, 2024 4 | 5 | This ToS applies applies to the Discord bot FixEmbed#5654. 6 | 7 | You may not use FixEmbed to violate any applicable laws or regulations, [Discord’s Terms of Service](https://discord.com/terms), or [Discord Community Guidelines](https://discord.com/guidelines). You also may not use FixEmbed to harm anyone or anything. 8 | 9 | When using bot commands that use external services, additional terms may apply to you. Here are ToSes that apply to certain commands: [X/Twitter](https://help.x.com/en/rules-and-policies), [TikTok](https://www.tiktok.com/legal/page/us/terms-of-service/en), [Reddit](https://www.redditinc.com/policies/content-policy), and [Instagram](https://about.instagram.com/blog/announcements/instagram-community-terms-of-use-faqs/). Do not use FixEmbed to violate the terms of any service. 10 | 11 | To contact me ([Kenneth Hendricks](https://github.com/kenhendricks00)), the developer of FixEmbed, you can mention strikermonkeyxd (1121099921655865375) in the official [FixEmbed Support Discord server](https://discord.gg/QFxTAmtZdn). 12 | 13 | Also see [FixEmbed’s Privacy Policy](https://github.com/kenhendricks00/FixEmbedBot/blob/main/privacy-policy.md). 14 | 15 | Thanks for using FixEmbed! 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Request a new feature. 3 | title: "[REQUEST] " 4 | labels: ["enhancement"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thank you for taking the time to suggest a new feature! 10 | - type: textarea 11 | id: problem-related 12 | attributes: 13 | label: Is your feature request related to a problem? Please describe. 14 | description: A clear and concise description of what the problem is. 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: solution 19 | attributes: 20 | label: Describe the solution you'd like 21 | description: A clear and concise description of what you want to happen. 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: alternatives 26 | attributes: 27 | label: Describe alternatives you've considered 28 | description: A clear and concise description of any alternative solutions or features you've considered. 29 | validations: 30 | required: false 31 | - type: textarea 32 | id: additional-context 33 | attributes: 34 | label: Additional context 35 | description: Add any other context or screenshots about the feature request here. 36 | validations: 37 | required: false 38 | -------------------------------------------------------------------------------- /privacy-policy.md: -------------------------------------------------------------------------------- 1 | # FixEmbed Privacy Policy 2 | 3 | Last updated and effective: July 8, 2024 4 | 5 | This privacy policy applies to the Discord bot FixEmbed#5654. 6 | 7 | By default, FixEmbed does not store any data. If a server member with the Manage Server permission uses the activate and deactivate command to change the bot’s state of the channel, then the server’s automatically-generated Discord ID (for example, 398998849026261003) will be stored in the bot’s SQL database along with the provided state, this goes for activation/deactivation via the settings command. The conversion service's states are stored in the bot's SQL database as well, so the bot knows what links to convert. No other data relating to the server is stored. FixEmbed never stores data specific to users. 8 | 9 | The state of the channel data is stored only for the bot to function and is not shared with anyone else or used for any other reason. The data is stored in a SQL database. 10 | 11 | To contact me ([Kenneth Hendricks](https://github.com/kenhendricks00)), the developer of FixEmbed, if you want the data to be removed or for any other reason, you can mention strikermonkeyxd (1121099921655865375) in the official [FixEmbed Support Discord server](https://discord.gg/QFxTAmtZdn). 12 | 13 | Also see [Discord’s Privacy Policy](https://discord.com/privacy). 14 | 15 | Thanks for using FixEmbed! 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a report to help us improve. Write in English, please. 3 | title: "[BUG] Write a title for your bug" 4 | labels: ["bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thank you for creating a bug report to help us improve! 10 | - type: textarea 11 | id: bug-description 12 | attributes: 13 | label: Describe the bug 14 | description: A clear and concise description of what the bug is. 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: bug-reproduce 19 | attributes: 20 | label: Steps to Reproduce 21 | description: Steps to reproduce the behavior. For example, "1. Go to '...', 2. Click on '...', 3. See error" 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: expected-behavior 26 | attributes: 27 | label: Expected behavior 28 | description: A clear and concise description of what you expected to happen. 29 | validations: 30 | required: false 31 | - type: textarea 32 | id: additional-info 33 | attributes: 34 | label: Additional information and data 35 | description: | 36 | If applicable, paste "FixEmbed Stats" here. 37 | Use the /settings command, then click "Debug" in the dropdown menu, then click the copy button in the "FixEmbed Stats" text area. 38 | validations: 39 | required: false 40 | - type: input 41 | id: fixembed-version 42 | attributes: 43 | label: FixEmbed Version 44 | description: Please provide the version of FixEmbed you are using, if current, just leave blank. 45 | validations: 46 | required: false 47 | - type: checkboxes 48 | id: terms 49 | attributes: 50 | label: Before opening this Issue 51 | options: 52 | - label: I have searched the issues of this repository and believe that this is not a duplicate. 53 | required: true 54 | -------------------------------------------------------------------------------- /docs/script.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function() { 2 | // Smooth scrolling for anchor links 3 | document.querySelectorAll('a[href^="#"]').forEach(anchor => { 4 | anchor.addEventListener('click', function(e) { 5 | e.preventDefault(); 6 | 7 | const targetId = this.getAttribute('href'); 8 | if (targetId === '#') return; 9 | 10 | const targetElement = document.querySelector(targetId); 11 | if (targetElement) { 12 | window.scrollTo({ 13 | top: targetElement.offsetTop - 80, 14 | behavior: 'smooth' 15 | }); 16 | } 17 | }); 18 | }); 19 | 20 | // Make features section visible immediately 21 | document.querySelectorAll('.feature-card').forEach(element => { 22 | element.style.opacity = '1'; 23 | element.style.transform = 'translateY(0)'; 24 | }); 25 | 26 | // Add scroll animation for other elements 27 | const animateOnScroll = function() { 28 | const elements = document.querySelectorAll('.platform-card, .support-card, .step'); 29 | 30 | elements.forEach(element => { 31 | const elementPosition = element.getBoundingClientRect().top; 32 | const screenPosition = window.innerHeight / 1.3; 33 | 34 | if (elementPosition < screenPosition) { 35 | element.style.opacity = '1'; 36 | element.style.transform = 'translateY(0)'; 37 | } 38 | }); 39 | }; 40 | 41 | // Apply initial styles for animations (excluding feature cards) 42 | const elementsToAnimate = document.querySelectorAll('.platform-card, .support-card, .step'); 43 | elementsToAnimate.forEach(element => { 44 | element.style.opacity = '0'; 45 | element.style.transform = 'translateY(20px)'; 46 | element.style.transition = 'opacity 0.5s ease, transform 0.5s ease'; 47 | }); 48 | 49 | // Run animation on scroll 50 | window.addEventListener('scroll', animateOnScroll); 51 | 52 | // Run once on load 53 | setTimeout(animateOnScroll, 100); 54 | }); 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |
5 | 6 | Invite Link 7 | 8 | Top.gg 9 | 10 | Last Commit 11 |
12 |
13 |

Enhance Your Discord with Proper Embeds for Social Media Links.

14 | 15 | # 🛠️ Usage 16 | Send a message containing a X/Twitter, Instagram, Reddit, Threads, Pixiv, Bluesky, YouTube link, and the bot will remove your message or just the embed and automatically convert it to it's fixed link respectively, replying with the fixed link and label of who sent it. 17 |

18 | 19 |

20 | You can toggle the link conversion on specific channels using /activate and /deactivate. 21 | 22 | # 🌟 Why Choose FixEmbed? 23 | - **Comprehensive Platform Support**: Supports X/Twitter, Instagram, Reddit, Threads, Pixiv, Bluesky, YouTube, and more. 24 | - **User-Friendly Configuration**: Easy setup with customizable settings for individual servers. 25 | - **Reliable Performance**: Ensures consistent embed functionality across all platforms. 26 | 27 | # 📋 Key Features 28 | 1. **Multi-Platform Support**: 29 | - **X/Twitter** 30 | - **Instagram** 31 | - **Reddit** 32 | - **Threads** 33 | - **Pixiv** 34 | - **Bluesky** 35 | - **YouTube** 36 | 2. **Customizable Settings**: 37 | - Activate or deactivate services per channel or server-wide. 38 | 3. **Direct Message Capability**: 39 | - Use the bot privately by sending links directly. 40 | 4. **Easy Hosting Options**: 41 | - Host the bot yourself using Docker. 42 | 43 | # 🚀 Invite FixEmbed to Your Server 44 | Click the following link to invite FixEmbed to your server: [Invite FixEmbed](https://discord.com/oauth2/authorize?client_id=1173820242305224764) 45 | 46 | # 🐳 Host FixEmbed Yourself 47 | You can host the bot yourself using Docker: 48 |
49 | ```bash 50 | docker pull kenhendricks00/fixembed 51 | docker run -d kenhendricks00/fixembed 52 | ``` 53 | Just don't forget to set your bot's token using BOT_TOKEN 54 | 55 | # 💬 Support 56 | If you need support or have any questions, you can join the [support server](https://discord.gg/QFxTAmtZdn) or open an issue on GitHub. 57 |
58 | **Note:** If it's a technical issue, be sure to have debug info ready by using /settings, then click Debug. 59 | 60 | # 🎉 Quick Links 61 | - [Invite FixEmbed](https://discord.com/oauth2/authorize?client_id=1173820242305224764) 62 | - [Vote for FixEmbed on Top.gg](https://top.gg/bot/1173820242305224764) 63 | - [Star our Source Code on GitHub](https://github.com/kenhendricks00/FixEmbed) 64 | - [Join the Support Server](https://discord.gg/QFxTAmtZdn) 65 | 66 | # 📜 Credits 67 | - [FxTwitter](https://github.com/FixTweet/FxTwitter), the service used to fix Twitter embeds, created by FixTweet 68 | - [InstaFix](https://github.com/Wikidepia/InstaFix), the service used to fix Instagram embeds, created by Wikidepia 69 | - [vxReddit](https://github.com/dylanpdx/vxReddit), the service used to fix Reddit embeds, created by dylanpdx 70 | - [fixthreads](https://github.com/milanmdev/fixthreads), the service used to fix Threads embeds, created by milanmdev 71 | - [phixiv](https://github.com/thelaao/phixiv), the service used to fix Pixiv embeds, created by thelaao 72 | - [VixBluesky](https://github.com/Rapougnac/VixBluesky), the service used to fix Bluesky embeds, created by Rapougnac 73 | - [koutube](https://github.com/iGerman00/koutube), the service used to fix YouTube embeds, created by iGerman00 74 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | FixEmbed - Fix Discord Embeds for Social Media 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 39 | 40 |
41 |
42 | 43 |
44 |
45 |

Fix your Discord embeds with one bot

46 |

FixEmbed automatically transforms social media links to proper embeds in Discord

47 | 51 |
52 |
53 | FixEmbed Discord Bot Preview 54 |
55 |
56 | 57 | 58 |
59 |

Fix your Discord embeds with one bot

60 |

FixEmbed automatically transforms social media links to proper embeds in Discord

61 | 65 |
66 | FixEmbed Discord Bot Preview 67 |
68 |
69 |
70 |
71 |
72 | 73 |
74 |
75 |

Why Choose FixEmbed?

76 |
77 |
78 |
79 |

Multi-Platform Support

80 |

Supports X/Twitter, Instagram, Reddit, Threads, Pixiv, Bluesky, YouTube, and more to come.

81 |
82 |
83 |
84 |

Customizable Settings

85 |

Easy setup with options to activate or deactivate services per channel or server-wide.

86 |
87 |
88 |
89 |

Reliable Performance

90 |

Ensures consistent embed functionality across all supported platforms.

91 |
92 |
93 |
94 |

Self-Hosting Option

95 |

Easily host the bot yourself using Docker for complete control.

96 |
97 |
98 |
99 |
100 | 101 |
102 |
103 |

How It Works

104 |
105 |
106 |
1
107 |
108 |

Add FixEmbed to your server

109 |

Invite the bot to your Discord server with just a few clicks.

110 |
111 |
112 |
113 |
2
114 |
115 |

Share social media links

116 |

Post links from X/Twitter, Instagram, Reddit, Threads, Pixiv, or Bluesky.

117 |
118 |
119 |
120 |
3
121 |
122 |

Watch the magic happen

123 |

FixEmbed will automatically replace your message with properly embedded content.

124 |
125 |
126 |
127 |
128 |
129 | 130 |
131 |
132 |

Supported Platforms

133 |
134 |
135 |
136 |

X / Twitter

137 |

Properly displays tweets with images, videos, and text.

138 |
139 |
140 |
141 |

Instagram

142 |

Shows posts, stories, and reels with full previews.

143 |
144 |
145 |
146 |

Reddit

147 |

Embeds posts and comments with complete formatting.

148 |
149 |
150 |
151 |

Threads

152 |

Displays Threads posts with proper media support.

153 |
154 |
155 |
156 |

Pixiv

157 |

Shows artwork and illustrations with previews.

158 |
159 |
160 |
161 |

Bluesky

162 |

Embeds Bluesky posts with full media support.

163 |
164 |
165 |
166 |

YouTube

167 |

Converts YouTube links for proper embed playback.

168 |
169 |
170 |
171 |
172 | 173 |
174 |
175 |
176 |

Ready to fix your Discord embeds?

177 |

Add FixEmbed to your server today and enjoy proper social media embeds!

178 | Add to Discord 179 |
180 |
181 |
182 | 183 |
184 |
185 |

Host It Yourself

186 |
187 |
188 |

You can easily host FixEmbed yourself using Docker:

189 |
190 |
docker pull kenhendricks00/fixembed
191 | docker run -d kenhendricks00/fixembed
192 |
193 |

Don't forget to set your bot's token using the BOT_TOKEN environment variable.

194 |
195 |
196 |
197 |
198 | 199 |
200 |
201 |

Support

202 |
203 |
204 |
205 |

Discord Server

206 |

Join our support server for help and updates.

207 | Join Server 208 |
209 |
210 |
211 |

GitHub Issues

212 |

Report bugs or request features on our GitHub.

213 | Open Issue 214 |
215 |
216 |
217 |

Debug Info

218 |

Use /settings in Discord, then click Debug for technical issues.

219 |
220 |
221 |
222 |
223 | 224 |
225 |
226 |

Credits

227 |

FixEmbed relies on these amazing services:

228 | 237 |
238 |
239 | 240 | 259 | 260 | 261 | 262 | 263 | -------------------------------------------------------------------------------- /docs/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-color: #5865F2; 3 | --secondary-color: #2C2F33; 4 | --accent-color: #99AAB5; 5 | --text-color: #FFFFFF; 6 | --background-color: #1A1C20; 7 | --card-bg-color: #2C2F33; 8 | --hover-color: #4752C4; 9 | --gradient-start: #5865F2; 10 | --gradient-end: #7289DA; 11 | } 12 | 13 | * { 14 | margin: 0; 15 | padding: 0; 16 | box-sizing: border-box; 17 | } 18 | 19 | body { 20 | font-family: 'Inter', sans-serif; 21 | line-height: 1.6; 22 | color: var(--text-color); 23 | background-color: var(--background-color); 24 | } 25 | 26 | .container { 27 | width: 100%; 28 | max-width: 1200px; 29 | margin: 0 auto; 30 | padding: 0 20px; 31 | } 32 | 33 | a { 34 | text-decoration: none; 35 | color: var(--text-color); 36 | transition: color 0.3s ease; 37 | } 38 | 39 | a:hover { 40 | color: var(--primary-color); 41 | } 42 | 43 | h1, h2, h3, h4, h5, h6 { 44 | font-weight: 700; 45 | margin-bottom: 0.5em; 46 | } 47 | 48 | h1 { 49 | font-size: 3rem; 50 | line-height: 1.2; 51 | } 52 | 53 | h2 { 54 | font-size: 2.5rem; 55 | text-align: center; 56 | margin-bottom: 2rem; 57 | } 58 | 59 | h3 { 60 | font-size: 1.5rem; 61 | } 62 | 63 | /* Header & Navigation */ 64 | header { 65 | background-color: var(--secondary-color); 66 | position: relative; 67 | overflow: hidden; 68 | } 69 | 70 | header::after { 71 | content: ''; 72 | position: absolute; 73 | bottom: 0; 74 | left: 0; 75 | width: 100%; 76 | height: 100px; 77 | background: linear-gradient(to bottom, rgba(26, 28, 32, 0), rgba(26, 28, 32, 1)); 78 | pointer-events: none; 79 | z-index: 1; 80 | } 81 | 82 | nav { 83 | padding: 20px 0; 84 | } 85 | 86 | .nav-container { 87 | display: flex; 88 | justify-content: space-between; 89 | align-items: center; 90 | } 91 | 92 | .logo { 93 | font-size: 1.8rem; 94 | font-weight: 700; 95 | color: var(--text-color); 96 | display: flex; 97 | align-items: center; 98 | gap: 10px; 99 | } 100 | 101 | .logo-img { 102 | height: 32px; 103 | width: auto; 104 | border-radius: 50%; 105 | } 106 | 107 | .nav-links { 108 | display: flex; 109 | gap: 20px; 110 | } 111 | 112 | .nav-links a { 113 | font-weight: 500; 114 | } 115 | 116 | .github-link { 117 | font-size: 1.2rem; 118 | } 119 | 120 | .invite-btn { 121 | background-color: var(--primary-color); 122 | color: var(--text-color); 123 | padding: 8px 16px; 124 | border-radius: 4px; 125 | font-weight: 500; 126 | transition: background-color 0.3s ease; 127 | } 128 | 129 | .invite-btn:hover { 130 | background-color: var(--hover-color); 131 | color: var(--text-color); 132 | } 133 | 134 | /* Hero Section */ 135 | .hero-section { 136 | padding: 100px 0; 137 | position: relative; 138 | } 139 | 140 | .hero-content { 141 | max-width: 50%; 142 | margin-right: 40px; 143 | position: relative; 144 | z-index: 2; 145 | } 146 | 147 | .subtitle { 148 | font-size: 1.2rem; 149 | margin-bottom: 2rem; 150 | color: var(--accent-color); 151 | } 152 | 153 | .cta-buttons { 154 | display: flex; 155 | gap: 20px; 156 | margin-top: 2rem; 157 | } 158 | 159 | .primary-btn { 160 | background-color: var(--primary-color); 161 | color: var(--text-color); 162 | padding: 12px 24px; 163 | border-radius: 4px; 164 | font-weight: 500; 165 | display: inline-block; 166 | transition: background-color 0.3s ease; 167 | } 168 | 169 | .primary-btn:hover { 170 | background-color: var(--hover-color); 171 | color: var(--text-color); 172 | } 173 | 174 | .secondary-btn { 175 | background-color: transparent; 176 | border: 1px solid var(--primary-color); 177 | color: var(--text-color); 178 | padding: 12px 24px; 179 | border-radius: 4px; 180 | font-weight: 500; 181 | display: inline-block; 182 | transition: background-color 0.3s ease, border-color 0.3s ease; 183 | } 184 | 185 | .secondary-btn:hover { 186 | background-color: rgba(88, 101, 242, 0.1); 187 | border-color: var(--hover-color); 188 | color: var(--text-color); 189 | } 190 | 191 | .hero-image { 192 | position: absolute; 193 | right: 23%; 194 | top: 50%; 195 | transform: translateY(-50%); 196 | max-width: 40%; 197 | border-radius: 8px; 198 | box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); 199 | overflow: hidden; 200 | } 201 | 202 | .hero-image img { 203 | width: 100%; 204 | height: auto; 205 | display: block; 206 | } 207 | 208 | /* Features Section */ 209 | .features-section { 210 | padding: 100px 0; 211 | background-color: var(--background-color); 212 | position: relative; 213 | } 214 | 215 | .features-grid { 216 | display: grid; 217 | grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); 218 | gap: 30px; 219 | margin-top: 40px; 220 | } 221 | 222 | .feature-card { 223 | background-color: var(--card-bg-color); 224 | padding: 30px; 225 | border-radius: 8px; 226 | transition: transform 0.3s ease, box-shadow 0.3s ease; 227 | } 228 | 229 | .feature-card:hover { 230 | transform: translateY(-5px); 231 | box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2); 232 | } 233 | 234 | .feature-icon { 235 | font-size: 2rem; 236 | color: var(--primary-color); 237 | margin-bottom: 20px; 238 | } 239 | 240 | /* How It Works */ 241 | .how-it-works-section { 242 | padding: 100px 0; 243 | background-color: var(--secondary-color); 244 | } 245 | 246 | .steps-container { 247 | display: flex; 248 | flex-direction: column; 249 | gap: 40px; 250 | margin-top: 40px; 251 | } 252 | 253 | .step { 254 | display: flex; 255 | align-items: flex-start; 256 | gap: 20px; 257 | } 258 | 259 | .step-number { 260 | background: linear-gradient(to bottom right, var(--gradient-start), var(--gradient-end)); 261 | width: 50px; 262 | height: 50px; 263 | border-radius: 50%; 264 | display: flex; 265 | align-items: center; 266 | justify-content: center; 267 | font-size: 1.5rem; 268 | font-weight: 700; 269 | flex-shrink: 0; 270 | } 271 | 272 | /* Platforms Section */ 273 | .platforms-section { 274 | padding: 100px 0; 275 | background-color: var(--background-color); 276 | } 277 | 278 | .platforms-grid { 279 | display: grid; 280 | grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); 281 | gap: 30px; 282 | margin-top: 40px; 283 | } 284 | 285 | .platform-card { 286 | background-color: var(--card-bg-color); 287 | padding: 30px; 288 | border-radius: 8px; 289 | text-align: center; 290 | transition: transform 0.3s ease, box-shadow 0.3s ease; 291 | } 292 | 293 | .platform-card:hover { 294 | transform: translateY(-5px); 295 | box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2); 296 | } 297 | 298 | .platform-icon { 299 | font-size: 2.5rem; 300 | color: var(--primary-color); 301 | margin-bottom: 20px; 302 | } 303 | 304 | /* CTA Section */ 305 | .cta-section { 306 | padding: 100px 0; 307 | background: linear-gradient(to right, var(--gradient-start), var(--gradient-end)); 308 | text-align: center; 309 | } 310 | 311 | .cta-content { 312 | max-width: 800px; 313 | margin: 0 auto; 314 | } 315 | 316 | .cta-content h2 { 317 | margin-bottom: 1rem; 318 | } 319 | 320 | .cta-content p { 321 | margin-bottom: 2rem; 322 | font-size: 1.2rem; 323 | } 324 | 325 | /* Hosting Section */ 326 | .hosting-section { 327 | padding: 100px 0; 328 | background-color: var(--secondary-color); 329 | } 330 | 331 | .hosting-content { 332 | display: flex; 333 | justify-content: center; 334 | } 335 | 336 | .hosting-text { 337 | max-width: 800px; 338 | background-color: var(--card-bg-color); 339 | padding: 30px; 340 | border-radius: 8px; 341 | } 342 | 343 | .code-block { 344 | background-color: rgba(0, 0, 0, 0.3); 345 | padding: 20px; 346 | border-radius: 4px; 347 | margin: 20px 0; 348 | overflow-x: auto; 349 | } 350 | 351 | .code-block pre { 352 | margin: 0; 353 | } 354 | 355 | code { 356 | font-family: 'Courier New', Courier, monospace; 357 | color: #FFFFFF; 358 | } 359 | 360 | .note { 361 | margin-top: 20px; 362 | font-style: italic; 363 | color: var(--accent-color); 364 | } 365 | 366 | /* Support Section */ 367 | .support-section { 368 | padding: 100px 0; 369 | background-color: var(--background-color); 370 | } 371 | 372 | .support-options { 373 | display: grid; 374 | grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); 375 | gap: 30px; 376 | margin-top: 40px; 377 | } 378 | 379 | .support-card { 380 | background-color: var(--card-bg-color); 381 | padding: 30px; 382 | border-radius: 8px; 383 | text-align: center; 384 | transition: transform 0.3s ease, box-shadow 0.3s ease; 385 | } 386 | 387 | .support-card:hover { 388 | transform: translateY(-5px); 389 | box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2); 390 | } 391 | 392 | .support-icon { 393 | font-size: 2.5rem; 394 | color: var(--primary-color); 395 | margin-bottom: 20px; 396 | } 397 | 398 | .support-link { 399 | display: inline-block; 400 | margin-top: 15px; 401 | color: var(--primary-color); 402 | font-weight: 500; 403 | } 404 | 405 | .support-link:hover { 406 | text-decoration: underline; 407 | } 408 | 409 | /* Credits Section */ 410 | .credits-section { 411 | padding: 100px 0; 412 | background-color: var(--secondary-color); 413 | text-align: center; 414 | } 415 | 416 | .credits-section p { 417 | margin-bottom: 30px; 418 | } 419 | 420 | .credits-grid { 421 | display: grid; 422 | grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); 423 | gap: 20px; 424 | max-width: 900px; 425 | margin: 0 auto; 426 | } 427 | 428 | .credit-link { 429 | background-color: var(--card-bg-color); 430 | padding: 15px; 431 | border-radius: 4px; 432 | transition: background-color 0.3s ease; 433 | } 434 | 435 | .credit-link:hover { 436 | background-color: var(--primary-color); 437 | color: var(--text-color); 438 | } 439 | 440 | /* Footer */ 441 | footer { 442 | background-color: var(--background-color); 443 | padding: 60px 0; 444 | border-top: 1px solid rgba(255, 255, 255, 0.1); 445 | } 446 | 447 | .footer-content { 448 | display: flex; 449 | justify-content: space-between; 450 | align-items: flex-start; 451 | flex-wrap: wrap; 452 | gap: 40px; 453 | } 454 | 455 | .footer-left { 456 | flex: 1; 457 | min-width: 300px; 458 | } 459 | 460 | .footer-right { 461 | flex: 1; 462 | min-width: 300px; 463 | } 464 | 465 | .footer-logo { 466 | font-size: 1.5rem; 467 | font-weight: 700; 468 | margin-bottom: 10px; 469 | display: block; 470 | } 471 | 472 | .footer-links { 473 | display: flex; 474 | gap: 20px; 475 | margin-bottom: 20px; 476 | } 477 | 478 | .copyright { 479 | color: var(--accent-color); 480 | font-size: 0.9rem; 481 | } 482 | 483 | /* Responsive Design */ 484 | /* Modify hero section base layout */ 485 | .hero-section { 486 | position: relative; 487 | overflow: visible; 488 | } 489 | 490 | .hero-section .container { 491 | position: relative; 492 | } 493 | 494 | /* Desktop hero styles */ 495 | .hero-desktop { 496 | display: flex; 497 | align-items: center; 498 | justify-content: space-between; 499 | position: relative; 500 | } 501 | 502 | /* Mobile hero styles - hidden by default */ 503 | .hero-mobile { 504 | display: none; 505 | flex-direction: column; 506 | text-align: center; 507 | } 508 | 509 | /* Force spacing between hero content and image */ 510 | .hero-content { 511 | max-width: 42%; 512 | padding-right: 3%; 513 | position: relative; 514 | z-index: 10; /* Ensure text is always on top */ 515 | } 516 | 517 | .hero-image { 518 | max-width: 40%; 519 | margin-left: 15%; 520 | border-radius: 8px; 521 | box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); 522 | overflow: hidden; 523 | z-index: 5; /* Lower than content */ 524 | } 525 | 526 | /* Large screens */ 527 | @media (min-width: 1201px) { 528 | .hero-image { 529 | position: absolute; 530 | right: 10%; 531 | top: 50%; 532 | transform: translateY(-50%); 533 | } 534 | } 535 | 536 | /* Small screens */ 537 | @media (max-width: 1024px) { 538 | .hero-section { 539 | padding: 60px 0; 540 | } 541 | 542 | /* Hide desktop, show mobile layout */ 543 | .hero-desktop { 544 | display: none; 545 | } 546 | 547 | .hero-mobile { 548 | display: flex; 549 | } 550 | 551 | .hero-image-mobile { 552 | width: 100%; 553 | max-width: 600px; 554 | margin: 0 auto; 555 | position: static; /* Static positioning to ensure natural stacking */ 556 | z-index: 1; /* Proper z-index */ 557 | display: block; /* Force block display */ 558 | } 559 | 560 | .hero-image-mobile img { 561 | width: 100%; 562 | height: auto; 563 | border-radius: 8px; 564 | box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); 565 | } 566 | 567 | .hero-mobile h1 { 568 | max-width: 100%; 569 | margin-right: 0; 570 | text-align: center; 571 | position: static; /* Static positioning to ensure natural stacking */ 572 | z-index: 2; /* Higher z-index than image */ 573 | padding-right: 0; /* Reset padding on mobile */ 574 | margin-bottom: 30px; /* Add space between content and image */ 575 | } 576 | 577 | /* Center CTA buttons on mobile */ 578 | .mobile-buttons { 579 | display: flex; 580 | justify-content: center; 581 | gap: 20px; 582 | margin-bottom: 30px; /* Increased space between buttons and image */ 583 | margin-top: 20px; 584 | } 585 | 586 | .container { 587 | padding: 0 30px; 588 | } 589 | } 590 | 591 | @media (max-width: 768px) { 592 | h1 { 593 | font-size: 2.5rem; 594 | } 595 | 596 | h2 { 597 | font-size: 2rem; 598 | } 599 | 600 | .nav-links { 601 | display: none; 602 | } 603 | 604 | .features-grid, 605 | .platforms-grid, 606 | .support-options, 607 | .credits-grid { 608 | grid-template-columns: 1fr; 609 | } 610 | 611 | .cta-buttons { 612 | flex-direction: column; 613 | } 614 | 615 | .footer-content { 616 | flex-direction: column; 617 | align-items: center; 618 | text-align: center; 619 | } 620 | 621 | .footer-links { 622 | justify-content: center; 623 | } 624 | } 625 | 626 | @media (min-width: 768px) and (max-width: 1024px) { 627 | .features-grid, 628 | .platforms-grid { 629 | grid-template-columns: repeat(2, 1fr); 630 | } 631 | } 632 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import asyncio 3 | from discord.ext import commands, tasks 4 | from discord import app_commands, ui 5 | from typing import Optional 6 | import discord 7 | import re 8 | import os 9 | from dotenv import load_dotenv 10 | import itertools 11 | import aiosqlite 12 | import sqlite3 13 | import time 14 | from collections import deque 15 | 16 | # Version number 17 | VERSION = "1.2.1" 18 | 19 | # Initialize logging 20 | logging.basicConfig(level=logging.INFO) 21 | 22 | # Bot configuration 23 | intents = discord.Intents.default() 24 | intents.message_content = True 25 | client = commands.Bot(command_prefix='/', intents=intents, shard_count=10) 26 | 27 | # In-memory storage for channel states and settings 28 | channel_states = {} 29 | bot_settings = {} 30 | 31 | # Rate-limiting configuration 32 | MESSAGE_LIMIT = 5 33 | TIME_WINDOW = 1 # Time window in seconds 34 | 35 | message_timestamps = deque() 36 | 37 | async def rate_limited_send(channel, content): 38 | current_time = time.time() 39 | while len(message_timestamps) >= MESSAGE_LIMIT and current_time - message_timestamps[0] < TIME_WINDOW: 40 | await asyncio.sleep(0.1) 41 | current_time = time.time() 42 | message_timestamps.popleft() 43 | message_timestamps.append(current_time) 44 | await channel.send(content) 45 | 46 | def create_footer(embed, client): 47 | embed.set_footer(text=f"{client.user.name} | v{VERSION}", icon_url=client.user.avatar.url) 48 | 49 | async def init_db(): 50 | db = await aiosqlite.connect('fixembed_data.db') 51 | await db.execute('''CREATE TABLE IF NOT EXISTS channel_states (channel_id INTEGER PRIMARY KEY, state BOOLEAN)''') 52 | await db.commit() 53 | await db.execute('''CREATE TABLE IF NOT EXISTS guild_settings (guild_id INTEGER PRIMARY KEY, enabled_services TEXT, mention_users BOOLEAN, delete_original BOOLEAN DEFAULT TRUE)''') 54 | await db.commit() 55 | 56 | try: 57 | await db.execute('ALTER TABLE guild_settings ADD COLUMN mention_users BOOLEAN DEFAULT TRUE') 58 | await db.commit() 59 | except sqlite3.OperationalError as e: 60 | if 'duplicate column name' in str(e): 61 | pass 62 | else: 63 | raise 64 | 65 | try: 66 | await db.execute('ALTER TABLE guild_settings ADD COLUMN delete_original BOOLEAN DEFAULT TRUE') 67 | await db.commit() 68 | except sqlite3.OperationalError as e: 69 | if 'duplicate column name' in str(e): 70 | pass 71 | else: 72 | raise 73 | 74 | return db 75 | 76 | async def load_channel_states(db): 77 | async with db.execute('SELECT channel_id, state FROM channel_states') as cursor: 78 | async for row in cursor: 79 | channel_states[row[0]] = row[1] 80 | 81 | for guild in client.guilds: 82 | for channel in guild.text_channels: 83 | if channel.id not in channel_states: 84 | channel_states[channel.id] = True 85 | 86 | async def load_settings(db): 87 | async with db.execute('SELECT guild_id, enabled_services, mention_users, delete_original FROM guild_settings') as cursor: 88 | async for row in cursor: 89 | guild_id, enabled_services, mention_users, delete_original = row 90 | enabled_services_list = eval(enabled_services) if enabled_services else ["Twitter", "Instagram", "Reddit", "Threads", "Pixiv", "Bluesky", "YouTube"] 91 | bot_settings[guild_id] = { 92 | "enabled_services": enabled_services_list, 93 | "mention_users": mention_users if mention_users is not None else True, 94 | "delete_original": delete_original if delete_original is not None else True 95 | } 96 | 97 | async def update_channel_state(db, channel_id, state): 98 | retries = 5 99 | for i in range(retries): 100 | try: 101 | await db.execute('INSERT OR REPLACE INTO channel_states (channel_id, state) VALUES (?, ?)', (channel_id, state)) 102 | await db.commit() 103 | break 104 | except sqlite3.OperationalError as e: 105 | if 'locked' in str(e): 106 | await asyncio.sleep(0.1) 107 | else: 108 | raise 109 | 110 | async def update_setting(db, guild_id, enabled_services, mention_users, delete_original): 111 | retries = 5 112 | for i in range(retries): 113 | try: 114 | await db.execute('INSERT OR REPLACE INTO guild_settings (guild_id, enabled_services, mention_users, delete_original) VALUES (?, ?, ?, ?)', (guild_id, repr(enabled_services), mention_users, delete_original)) 115 | await db.commit() 116 | break 117 | except sqlite3.OperationalError as e: 118 | if 'locked' in str(e): 119 | await asyncio.sleep(0.1) 120 | else: 121 | raise 122 | 123 | @client.event 124 | async def on_ready(): 125 | print(f'We have logged in as {client.user}') 126 | logging.info(f'Logged in as {client.user}') 127 | client.db = await init_db() 128 | await load_channel_states(client.db) 129 | await load_settings(client.db) 130 | change_status.start() 131 | 132 | try: 133 | synced = await client.tree.sync() 134 | print(f'Synced {len(synced)} command(s)') 135 | except Exception as e: 136 | print(f'Failed to sync commands: {e}') 137 | 138 | client.launch_time = discord.utils.utcnow() 139 | 140 | statuses = itertools.cycle([ 141 | "for Twitter links", "for Reddit links", "for Instagram links", "for Threads links", "for Pixiv links", "for Bluesky links", "for YouTube links" 142 | ]) 143 | 144 | @tasks.loop(seconds=60) 145 | async def change_status(): 146 | current_status = next(statuses) 147 | try: 148 | await client.change_presence(activity=discord.Activity(type=discord.ActivityType.watching, name=current_status)) 149 | except discord.errors.HTTPException as e: 150 | logging.error(f"Failed to change status: {e}") 151 | 152 | @client.tree.command( 153 | name='activate', 154 | description="Activate link processing in this channel or another channel") 155 | @app_commands.describe( 156 | channel="The channel to activate link processing in (leave blank for current channel)" 157 | ) 158 | async def activate(interaction: discord.Interaction, 159 | channel: Optional[discord.TextChannel] = None): 160 | if not channel: 161 | channel = interaction.channel 162 | channel_states[channel.id] = True 163 | await update_channel_state(client.db, channel.id, True) 164 | embed = discord.Embed(title=f"{client.user.name}", 165 | description=f'✅ Activated for {channel.mention}!', 166 | color=discord.Color(0x78b159)) 167 | create_footer(embed, client) 168 | await interaction.response.send_message(embed=embed) 169 | 170 | @client.tree.command( 171 | name='deactivate', 172 | description="Deactivate link processing in this channel or another channel") 173 | @app_commands.describe( 174 | channel="The channel to deactivate link processing in (leave blank for current channel)" 175 | ) 176 | async def deactivate(interaction: discord.Interaction, 177 | channel: Optional[discord.TextChannel] = None): 178 | if not channel: 179 | channel = interaction.channel 180 | channel_states[channel.id] = False 181 | await update_channel_state(client.db, channel.id, False) 182 | embed = discord.Embed(title=f"{client.user.name}", 183 | description=f'❌ Deactivated for {channel.mention}!', 184 | color=discord.Color.red()) 185 | create_footer(embed, client) 186 | await interaction.response.send_message(embed=embed) 187 | 188 | @client.tree.command( 189 | name='about', 190 | description="Show information about the bot") 191 | async def about(interaction: discord.Interaction): 192 | embed = discord.Embed( 193 | title="About", 194 | description="This bot fixes the lack of embed support in Discord.", 195 | color=discord.Color(0x7289DA)) 196 | embed.add_field( 197 | name="🎉 Quick Links", 198 | value=( 199 | "- [Invite FixEmbed](https://discord.com/oauth2/authorize?client_id=1173820242305224764)\n" 200 | "- [Vote for FixEmbed on Top.gg](https://top.gg/bot/1173820242305224764)\n" 201 | "- [Star our Source Code on GitHub](https://github.com/kenhendricks00/FixEmbedBot)\n" 202 | "- [Join the Support Server](https://discord.gg/QFxTAmtZdn)" 203 | ), 204 | inline=False) 205 | embed.add_field( 206 | name="📜 Credits", 207 | value=( 208 | "- [FxEmbed](https://github.com/FxEmbed/FxEmbed), created by FxEmbed\n" 209 | "- [InstagramEmbed](https://github.com/Lainmode/InstagramEmbed-vxinstagram), created by Lainmode\n" 210 | "- [vxReddit](https://github.com/dylanpdx/vxReddit), created by dylanpdx\n" 211 | "- [fixthreads](https://github.com/milanmdev/fixthreads), created by milanmdev\n" 212 | "- [phixiv](https://github.com/thelaao/phixiv), created by thelaao\n" 213 | "- [VixBluesky](https://github.com/Rapougnac/VixBluesky), created by Rapougnac\n" 214 | "- [koutube](https://github.com/iGerman00/koutube), created by iGerman00" 215 | ), 216 | inline=False) 217 | create_footer(embed, client) 218 | await interaction.response.send_message(embed=embed) 219 | 220 | async def debug_info(interaction: discord.Interaction, channel: Optional[discord.TextChannel] = None): 221 | if not channel: 222 | channel = interaction.channel 223 | 224 | guild = interaction.guild 225 | permissions = channel.permissions_for(guild.me) 226 | fix_embed_status = channel_states.get(channel.id, True) 227 | fix_embed_activated = all(channel_states.get(ch.id, True) for ch in guild.text_channels) 228 | 229 | embed = discord.Embed( 230 | title="Debug Information", 231 | description="For more help, join the [support server](https://discord.gg/QFxTAmtZdn)", 232 | color=discord.Color(0x7289DA)) 233 | 234 | embed.add_field( 235 | name="Status and Permissions", 236 | value=( 237 | f'{f"🟢 **FixEmbed working in** {channel.mention}" if fix_embed_status else f"🔴 **FixEmbed not working in** {channel.mention}"}\n' 238 | f"- {'🟢 FixEmbed activated' if fix_embed_status else '🔴 FixEmbed deactivated'}\n" 239 | f"- {'🟢' if permissions.read_messages else '🔴'} Read message permission\n" 240 | f"- {'🟢' if permissions.send_messages else '🔴'} Send message permission\n" 241 | f"- {'🟢' if permissions.embed_links else '🔴'} Embed links permission\n" 242 | f"- {'🟢' if permissions.manage_messages else '🔴'} Manage messages permission" 243 | ), 244 | inline=False 245 | ) 246 | 247 | shard_id = client.shard_id if client.shard_id is not None else 0 248 | embed.add_field( 249 | name="FixEmbed Stats", 250 | value=( 251 | f"```\n" 252 | f"Status: {'Activated' if fix_embed_activated else 'Deactivated'}\n" 253 | f"Shard: {shard_id + 1}\n" 254 | f"Uptime: {str(discord.utils.utcnow() - client.launch_time).split('.')[0]}\n" 255 | f"Version: {VERSION}\n" 256 | f"```" 257 | ), 258 | inline=False 259 | ) 260 | 261 | create_footer(embed, client) 262 | await interaction.response.send_message(embed=embed, view=SettingsView(interaction, bot_settings.get(interaction.guild.id, {"enabled_services": ["Twitter", "Instagram", "Reddit", "Threads", "Pixiv", "Bluesky", "YouTube"], "mention_users": True, "delete_original": True}))) 263 | 264 | class SettingsDropdown(ui.Select): 265 | 266 | def __init__(self, interaction, settings): 267 | self.interaction = interaction 268 | self.settings = settings 269 | activated = all( 270 | channel_states.get(ch.id, True) 271 | for ch in interaction.guild.text_channels) 272 | mention_users = settings.get("mention_users", True) 273 | delete_original = settings.get("delete_original", True) 274 | 275 | options = [ 276 | discord.SelectOption( 277 | label="FixEmbed", 278 | description="Activate or deactivate the bot in all channels", 279 | emoji="🟢" if activated else "🔴" 280 | ), 281 | discord.SelectOption( 282 | label="Mention Users", 283 | description="Toggle mentioning users in messages", 284 | emoji="🔔" if mention_users else "🔕" 285 | ), 286 | discord.SelectOption( 287 | label="Delivery Method", 288 | description="Toggle original message deletion", 289 | emoji="📬" if delete_original else "📪" 290 | ), 291 | discord.SelectOption( 292 | label="Service Settings", 293 | description="Configure which services are activated", 294 | emoji="⚙️"), 295 | discord.SelectOption( 296 | label="Debug", 297 | description="Show current debug information", 298 | emoji="🐞" 299 | ) 300 | ] 301 | super().__init__(placeholder="Choose an option...", 302 | min_values=1, 303 | max_values=1, 304 | options=options) 305 | 306 | async def callback(self, interaction: discord.Interaction): 307 | if self.values[0] == "Delivery Method": 308 | delete_original = self.settings.get("delete_original", True) 309 | embed = discord.Embed( 310 | title="Delivery Method Settings", 311 | description=f"Original message deletion is currently {'activated' if delete_original else 'deactivated'}.", 312 | color=discord.Color.green() if delete_original else discord.Color.red()) 313 | view = DeliveryMethodSettingsView(delete_original, self.interaction, self.settings) 314 | await interaction.response.send_message(embed=embed, view=view, ephemeral=True) 315 | elif self.values[0] == "FixEmbed": 316 | activated = all( 317 | channel_states.get(ch.id, True) 318 | for ch in interaction.guild.text_channels) 319 | embed = discord.Embed( 320 | title="FixEmbed Settings", 321 | description="**Activate/Deactivate FixEmbed:**\n" 322 | f"{'🟢 FixEmbed activated' if activated else '🔴 FixEmbed deactivated'}\n\n" 323 | "**NOTE:** May take a few seconds to apply changes to all channels.", 324 | color=discord.Color.green() 325 | if activated else discord.Color.red()) 326 | view = FixEmbedSettingsView(activated, self.interaction, self.settings) 327 | await interaction.response.send_message(embed=embed, view=view, ephemeral=True) 328 | elif self.values[0] == "Mention Users": 329 | mention_users = self.settings.get("mention_users", True) 330 | embed = discord.Embed( 331 | title="Mention Users Settings", 332 | description=f"User mentions are currently {'activated' if mention_users else 'deactivated'}.", 333 | color=discord.Color.green() if mention_users else discord.Color.red()) 334 | view = MentionUsersSettingsView(mention_users, self.interaction, self.settings) 335 | await interaction.response.send_message(embed=embed, view=view, ephemeral=True) 336 | elif self.values[0] == "Service Settings": 337 | enabled_services = self.settings.get("enabled_services", ["Twitter", "Instagram", "Reddit", "Threads", "Pixiv", "Bluesky", "YouTube"]) 338 | service_status_list = "\n".join([ 339 | f"{'🟢' if service in enabled_services else '🔴'} {service}" 340 | for service in ["Twitter", "Instagram", "Reddit", "Threads", "Pixiv", "Bluesky", "YouTube"] 341 | ]) 342 | embed = discord.Embed( 343 | title="Service Settings", 344 | description=f"Configure which services are activated.\n\n**Activated services:**\n{service_status_list}", 345 | color=discord.Color.blurple()) 346 | view = ServiceSettingsView(self.interaction, self.settings) 347 | await interaction.response.send_message(embed=embed, view=view, ephemeral=True) 348 | elif self.values[0] == "Debug": 349 | await debug_info(interaction, interaction.channel) 350 | 351 | 352 | class ServicesDropdown(ui.Select): 353 | 354 | def __init__(self, interaction, parent_view, settings): 355 | self.interaction = interaction 356 | self.parent_view = parent_view 357 | self.settings = settings 358 | enabled_services = settings.get("enabled_services", ["Twitter", "Instagram", "Reddit", "Threads", "Pixiv", "Bluesky", "YouTube"]) 359 | options = [ 360 | discord.SelectOption( 361 | label=service, 362 | description=f"Activate or deactivate {service} links", 363 | emoji="✅" if service in enabled_services else "❌") 364 | for service in ["Twitter", "Instagram", "Reddit", "Threads", "Pixiv", "Bluesky", "YouTube"] 365 | ] 366 | super().__init__(placeholder="Select services to activate...", 367 | min_values=1, 368 | max_values=len(options), 369 | options=options) 370 | 371 | async def callback(self, interaction: discord.Interaction): 372 | selected_services = self.values 373 | guild_id = self.interaction.guild.id 374 | self.settings["enabled_services"] = selected_services 375 | await update_setting(client.db, guild_id, selected_services, self.settings["mention_users"], self.settings["delete_original"]) 376 | 377 | self.parent_view.clear_items() 378 | self.parent_view.add_item( 379 | ServicesDropdown(self.interaction, self.parent_view, self.settings)) 380 | self.parent_view.add_item(SettingsDropdown(self.interaction, self.settings)) 381 | 382 | service_status_list = "\n".join([ 383 | f"{'🟢' if service in selected_services else '🔴'} {service}" 384 | for service in ["Twitter", "Instagram", "Reddit", "Threads", "Pixiv", "Bluesky", "YouTube"] 385 | ]) 386 | embed = discord.Embed( 387 | title="Service Settings", 388 | description=f"Configure which services are activated.\n\n**Activated services:**\n{service_status_list}", 389 | color=discord.Color.blurple()) 390 | 391 | try: 392 | await interaction.response.edit_message(embed=embed, view=self.parent_view) 393 | except discord.errors.NotFound: 394 | try: 395 | await interaction.edit_original_response(embed=embed, view=self.parent_view) 396 | except discord.errors.NotFound: 397 | logging.error("Failed to edit original response: Unknown Webhook") 398 | except discord.errors.InteractionResponded: 399 | try: 400 | await interaction.edit_original_response(embed=embed, view=self.parent_view) 401 | except discord.errors.NotFound: 402 | logging.error("Failed to edit original response: Unknown Webhook") 403 | 404 | class SettingsView(ui.View): 405 | 406 | def __init__(self, interaction, settings): 407 | super().__init__() 408 | self.add_item(SettingsDropdown(interaction, settings)) 409 | 410 | class ServiceSettingsView(ui.View): 411 | 412 | def __init__(self, interaction, settings): 413 | super().__init__() 414 | self.add_item(ServicesDropdown(interaction, self, settings)) 415 | self.add_item(SettingsDropdown(interaction, settings)) 416 | 417 | class FixEmbedSettingsView(ui.View): 418 | 419 | def __init__(self, activated, interaction, settings, timeout=180): 420 | super().__init__(timeout=timeout) 421 | self.activated = activated 422 | self.interaction = interaction 423 | self.settings = settings 424 | self.toggle_button = discord.ui.Button( 425 | label="Activated" if activated else "Deactivated", 426 | style=discord.ButtonStyle.green if activated else discord.ButtonStyle.red) 427 | self.toggle_button.callback = self.toggle 428 | self.add_item(self.toggle_button) 429 | self.add_item(SettingsDropdown(interaction, settings)) 430 | 431 | async def toggle(self, interaction: discord.Interaction): 432 | await interaction.response.defer() 433 | 434 | self.activated = not self.activated 435 | for ch in self.interaction.guild.text_channels: 436 | channel_states[ch.id] = self.activated 437 | await update_channel_state(client.db, ch.id, self.activated) 438 | self.toggle_button.label = "Activated" if self.activated else "Deactivated" 439 | self.toggle_button.style = discord.ButtonStyle.green if self.activated else discord.ButtonStyle.red 440 | 441 | embed = discord.Embed( 442 | title="FixEmbed Settings", 443 | description="**Activate/Deactivate FixEmbed:**\n" 444 | f"{'🟢 FixEmbed activated' if self.activated else '🔴 FixEmbed deactivated'}\n\n" 445 | "**NOTE:** May take a few seconds to apply changes to all channels.", 446 | color=discord.Color.green() if self.activated else discord.Color.red()) 447 | 448 | try: 449 | await interaction.edit_original_response(embed=embed, view=self, ephemeral=True) 450 | except discord.errors.NotFound: 451 | logging.error("Failed to edit original response: Unknown Webhook") 452 | 453 | async def on_timeout(self): 454 | for item in self.children: 455 | item.disabled = True 456 | 457 | embed = discord.Embed( 458 | title="FixEmbed Settings", 459 | description="This view has timed out and is no longer interactive.", 460 | color=discord.Color.red()) 461 | 462 | try: 463 | await self.interaction.edit_original_response(embed=embed, view=self, ephemeral=True) 464 | except discord.errors.NotFound: 465 | logging.error("Failed to edit original response on timeout: Unknown Webhook") 466 | 467 | 468 | class MentionUsersSettingsView(ui.View): 469 | 470 | def __init__(self, mention_users, interaction, settings, timeout=180): 471 | super().__init__(timeout=timeout) 472 | self.mention_users = mention_users 473 | self.interaction = interaction 474 | self.settings = settings 475 | self.toggle_button = discord.ui.Button( 476 | label="Activated" if mention_users else "Deactivated", 477 | style=discord.ButtonStyle.green if mention_users else discord.ButtonStyle.red) 478 | self.toggle_button.callback = self.toggle 479 | self.add_item(self.toggle_button) 480 | self.add_item(SettingsDropdown(interaction, settings)) 481 | 482 | async def toggle(self, interaction: discord.Interaction): 483 | await interaction.response.defer() 484 | 485 | self.mention_users = not self.mention_users 486 | self.settings["mention_users"] = self.mention_users 487 | await update_setting(client.db, self.interaction.guild.id, self.settings["enabled_services"], self.mention_users, self.settings["delete_original"]) 488 | self.toggle_button.label = "Activated" if self.mention_users else "Deactivated" 489 | self.toggle_button.style = discord.ButtonStyle.green if self.mention_users else discord.ButtonStyle.red 490 | 491 | embed = discord.Embed( 492 | title="Mention Users Settings", 493 | description=f"User mentions are currently {'activated' if self.mention_users else 'deactivated'}.", 494 | color=discord.Color.green() if self.mention_users else discord.Color.red()) 495 | 496 | try: 497 | await interaction.edit_original_response(embed=embed, view=self) 498 | except discord.errors.NotFound: 499 | logging.error("Failed to edit original response: Unknown Webhook") 500 | 501 | async def on_timeout(self): 502 | for item in self.children: 503 | item.disabled = True 504 | 505 | embed = discord.Embed( 506 | title="Mention Users Settings", 507 | description="This view has timed out and is no longer interactive.", 508 | color=discord.Color.red()) 509 | 510 | try: 511 | await self.interaction.edit_original_response(embed=embed, view=self) 512 | except discord.errors.NotFound: 513 | logging.error("Failed to edit original response on timeout: Unknown Webhook") 514 | 515 | class DeliveryMethodSettingsView(ui.View): 516 | 517 | def __init__(self, delete_original, interaction, settings, timeout=180): 518 | super().__init__(timeout=timeout) 519 | self.delete_original = delete_original 520 | self.interaction = interaction 521 | self.settings = settings 522 | self.toggle_button = discord.ui.Button( 523 | label="Activated" if delete_original else "Deactivated", 524 | style=discord.ButtonStyle.green if delete_original else discord.ButtonStyle.red) 525 | self.toggle_button.callback = self.toggle 526 | self.add_item(self.toggle_button) 527 | self.add_item(SettingsDropdown(interaction, settings)) 528 | 529 | async def toggle(self, interaction: discord.Interaction): 530 | await interaction.response.defer() 531 | 532 | self.delete_original = not self.delete_original 533 | self.settings["delete_original"] = self.delete_original 534 | await update_setting(client.db, self.interaction.guild.id, self.settings["enabled_services"], self.settings["mention_users"], self.delete_original) 535 | 536 | self.toggle_button.label = "Activated" if self.delete_original else "Deactivated" 537 | self.toggle_button.style = discord.ButtonStyle.green if self.delete_original else discord.ButtonStyle.red 538 | 539 | embed = discord.Embed( 540 | title="Delivery Method Setting", 541 | description=f"Original message deletion is now {'activated' if self.delete_original else 'deactivated'}.", 542 | color=discord.Color.green() if self.delete_original else discord.Color.red()) 543 | 544 | try: 545 | await interaction.edit_original_response(embed=embed, view=self) 546 | except discord.errors.NotFound: 547 | logging.error("Failed to edit original response: Unknown Webhook") 548 | 549 | async def on_timeout(self): 550 | for item in self.children: 551 | item.disabled = True 552 | 553 | embed = discord.Embed( 554 | title="Delivery Method Setting", 555 | description="This view has timed out and is no longer interactive.", 556 | color=discord.Color.red()) 557 | 558 | try: 559 | await self.interaction.edit_original_response(embed=embed, view=self) 560 | except discord.errors.NotFound: 561 | logging.error("Failed to edit original response on timeout: Unknown Webhook") 562 | 563 | @client.tree.command(name='settings', description="Configure FixEmbed's settings") 564 | async def settings(interaction: discord.Interaction): 565 | guild_id = interaction.guild.id 566 | guild_settings = bot_settings.get(guild_id, {"enabled_services": ["Twitter", "Instagram", "Reddit", "Threads", "Pixiv", "Bluesky", "YouTube"], "mention_users": True, "delete_original": True}) 567 | 568 | embed = discord.Embed(title="Settings", 569 | description="Configure FixEmbed's settings", 570 | color=discord.Color.blurple()) 571 | create_footer(embed, client) 572 | await interaction.response.send_message(embed=embed, view=SettingsView(interaction, guild_settings), ephemeral=True) 573 | 574 | @client.event 575 | async def on_message(message): 576 | if message.author == client.user: 577 | return 578 | 579 | guild_id = message.guild.id 580 | guild_settings = bot_settings.get(guild_id, { 581 | "enabled_services": ["Twitter", "Instagram", "Reddit", "Threads", "Pixiv", "Bluesky", "YouTube"], 582 | "mention_users": True, 583 | "delete_original": True 584 | }) 585 | enabled_services = guild_settings.get("enabled_services", ["Twitter", "Instagram", "Reddit", "Threads", "Pixiv", "Bluesky", "YouTube"]) 586 | mention_users = guild_settings.get("mention_users", True) 587 | delete_original = guild_settings.get("delete_original", True) 588 | 589 | if channel_states.get(message.channel.id, True): 590 | try: 591 | # Standard link pattern to capture all the relevant links 592 | link_pattern = r"https?://(?:www\.)?(twitter\.com/\w+/status/\d+|x\.com/\w+/status/\d+|instagram\.com/(?:p|reel)/[\w-]+|reddit\.com/r/\w+/s/\w+|reddit\.com/r/\w+/comments/\w+/\w+|old\.reddit\.com/r/\w+/comments/\w+/\w+|pixiv\.net/(?:en/)?artworks/\d+|threads\.net/@[^/]+/post/[\w-]+|bsky\.app/profile/[^/]+/post/[\w-]+|youtube\.com/watch\?v=[\w-]+|youtu\.be/[\w-]+)" 593 | matches = re.findall(link_pattern, message.content) 594 | 595 | # Regex pattern to detect links surrounded by < > 596 | surrounded_link_pattern = r"" 597 | 598 | valid_link_found = False 599 | 600 | for original_link in matches: 601 | # Skip links if they are surrounded by < > 602 | if re.search(surrounded_link_pattern, message.content): 603 | continue # Skip processing this link 604 | 605 | display_text = "" 606 | modified_link = original_link 607 | service = "" 608 | user_or_community = "" 609 | 610 | if 'twitter.com' in original_link or 'x.com' in original_link: 611 | service = "Twitter" 612 | user_match = re.findall( 613 | r"(?:twitter\.com|x\.com)/(\w+)/status/\d+", 614 | original_link) 615 | user_or_community = user_match[ 616 | 0] if user_match else "Unknown" 617 | 618 | elif 'instagram.com' in original_link: 619 | service = "Instagram" 620 | user_match = re.findall(r"instagram\.com/(?:p|reel)/([\w-]+)", 621 | original_link) 622 | user_or_community = user_match[ 623 | 0] if user_match else "Unknown" 624 | 625 | elif 'reddit.com' in original_link or 'old.reddit.com' in original_link: 626 | service = "Reddit" 627 | community_match = re.findall( 628 | r"(?:reddit\.com|old\.reddit\.com)/r/(\w+)", original_link) 629 | user_or_community = community_match[ 630 | 0] if community_match else "Unknown" 631 | 632 | elif 'pixiv.net' in original_link: 633 | service = "Pixiv" 634 | user_match = re.findall(r"pixiv\.net/(?:en/)?artworks/(\d+)", original_link) 635 | user_or_community = user_match[ 636 | 0] if user_match else "Unknown" 637 | 638 | elif 'threads.net' in original_link: 639 | service = "Threads" 640 | user_match = re.findall(r"threads\.net/@([^/]+)/post/([\w-]+)", original_link) 641 | if len(user_match) > 0: 642 | user_or_community, post_id = user_match[0] 643 | modified_link = f"fixthreads.net/@{user_or_community}/post/{post_id}" 644 | display_text = f"Threads • @{user_or_community}" 645 | 646 | elif 'bsky.app' in original_link: 647 | service = "Bluesky" 648 | bsky_match = re.findall(r"bsky\.app/profile/([^/]+)/post/([\w-]+)", original_link) 649 | if len(bsky_match) > 0: 650 | user_or_community, post_id = bsky_match[0] 651 | modified_link = f"bskyx.app/profile/{user_or_community}/post/{post_id}" 652 | display_text = f"Bluesky • {user_or_community}" 653 | 654 | elif 'youtube.com' in original_link or 'youtu.be' in original_link: 655 | service = "YouTube" 656 | if 'youtube.com' in original_link: 657 | video_id_match = re.findall(r"youtube\.com/watch\?v=([\w-]+)", original_link) 658 | if video_id_match: 659 | video_id = video_id_match[0] 660 | modified_link = f"koutube.com/watch?v={video_id}" 661 | display_text = f"YouTube • {video_id}" 662 | elif 'youtu.be' in original_link: 663 | video_id_match = re.findall(r"youtu\.be/([\w-]+)", original_link) 664 | if video_id_match: 665 | video_id = video_id_match[0] 666 | modified_link = f"koutube.com/watch?v={video_id}" 667 | display_text = f"YouTube • {video_id}" 668 | 669 | if service and user_or_community and service in enabled_services: 670 | if not display_text: 671 | display_text = f"{service} • {user_or_community}" 672 | modified_link = original_link.replace("twitter.com", "fxtwitter.com")\ 673 | .replace("x.com", "fixupx.com")\ 674 | .replace("instagram.com", "d.vxinstagram.com")\ 675 | .replace("reddit.com", "vxreddit.com")\ 676 | .replace("old.reddit.com", "vxreddit.com")\ 677 | .replace("threads.net", "fixthreads.net")\ 678 | .replace("pixiv.net", "phixiv.net")\ 679 | .replace("bsky.app", "bskyx.app")\ 680 | .replace("youtube.com", "koutube.com")\ 681 | .replace("youtu.be", "koutube.com/watch?v=") 682 | valid_link_found = True 683 | 684 | if valid_link_found: 685 | if delete_original: 686 | formatted_message = f"[{display_text}](https://{modified_link})" 687 | if mention_users: 688 | formatted_message += f" | Sent by {message.author.mention}" 689 | else: 690 | formatted_message += f" | Sent by {message.author.display_name}" 691 | await rate_limited_send(message.channel, formatted_message) 692 | await message.delete() 693 | else: 694 | await message.edit(suppress=True) 695 | formatted_message = f"[{display_text}](https://{modified_link})" 696 | await rate_limited_send(message.channel, formatted_message) 697 | 698 | except Exception as e: 699 | logging.error(f"Error in on_message: {e}") 700 | 701 | await client.process_commands(message) 702 | 703 | @client.event 704 | async def on_guild_join(guild): 705 | guild_id = guild.id 706 | if guild_id not in bot_settings: 707 | bot_settings[guild_id] = { 708 | "enabled_services": ["Twitter", "Instagram", "Reddit", "Threads", "Pixiv", "Bluesky", "YouTube"], 709 | "mention_users": True, 710 | "delete_original": True 711 | } 712 | await update_setting(client.db, guild_id, bot_settings[guild_id]["enabled_services"], bot_settings[guild_id]["mention_users"], bot_settings[guild_id]["delete_original"]) 713 | 714 | # Loading the bot token from .env 715 | load_dotenv() 716 | bot_token = os.getenv('BOT_TOKEN') 717 | client.run(bot_token) 718 | 719 | --------------------------------------------------------------------------------