├── .github ├── FUNDING.yml └── workflows │ ├── crowdin-download.yml │ ├── crowdin-upload.yml │ └── release.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── assets ├── FixTweetDemo.mp4 ├── apple_music.webp ├── bilibili.webp ├── bluesky.webp ├── deviantart.webp ├── facebook.webp ├── furaffinity.webp ├── ifunny.webp ├── instagram.webp ├── logo.pdn ├── logo.png ├── logo_alpha.png ├── logo_dev.png ├── logo_rounded.png ├── mastodon.webp ├── newgrounds.webp ├── nitter.webp ├── pixiv.webp ├── reddit.webp ├── settings.gif ├── showcase.png ├── snapchat.webp ├── spotify.webp ├── threads.webp ├── tiktok.webp ├── tumblr.webp ├── twitch.webp ├── twitter.webp ├── usage.png ├── youtube.webp ├── youtube_music.webp └── youtube_shorts.webp ├── cogs ├── commands.py ├── developer.py ├── link_fix.py └── setup.py ├── config.yml ├── database ├── config.py ├── migrations │ ├── 2023_11_17_092808_guilds_table.py │ ├── 2023_11_17_094206_text_channels_table.py │ ├── 2023_11_19_180926_import_json.py │ ├── 2024_06_19_182807_guild_settings.py │ ├── 2024_06_22_154629_channels_enabled.py │ ├── 2024_06_22_155714_websites_option.py │ ├── 2024_06_24_175057_members_table.py │ ├── 2024_06_25_131246_custom_websites_table.py │ ├── 2024_08_29_005648_default_states.py │ ├── 2024_08_30_133515_roles_table.py │ ├── 2024_08_30_143125_default_role_state.py │ ├── 2024_09_02_143111_websites_and_views.py │ ├── 2024_11_02_214523_remove_instagram_view.py │ ├── 2024_11_02_220821_add_bluesky_view.py │ ├── 2025_01_06_222619_new_bluesky_view.py │ ├── 2025_01_11_221023_add_snapchat.py │ ├── 2025_01_26_140544_refacto_members.py │ ├── 2025_01_26_223431_add_more_websites.py │ ├── 2025_03_16_145300_add_twitch_spotify.py │ ├── 2025_03_16_211708_create_events.py │ ├── 2025_03_16_234024_add_bot_members.py │ ├── 2025_03_18_133740_add_webhook_support.py │ └── 2025_06_03_021604_add_silent_replies.py └── models │ ├── CustomWebsite.py │ ├── Event.py │ ├── Guild.py │ ├── Member.py │ ├── Role.py │ └── TextChannel.py ├── docker-compose.example.yml ├── docker-entrypoint.sh ├── locales ├── bg.yml ├── cs.yml ├── da.yml ├── de.yml ├── el.yml ├── en-GB.yml ├── en-US.yml ├── es-419.yml ├── es-ES.yml ├── fi.yml ├── fr.yml ├── hi.yml ├── hr.yml ├── hu.yml ├── id.yml ├── it.yml ├── ja.yml ├── ko.yml ├── lt.yml ├── nl.yml ├── no.yml ├── pl.yml ├── pr-BR.yml ├── ro.yml ├── ru.yml ├── sv-SE.yml ├── th.yml ├── tr.yml ├── uk.yml ├── vi.yml ├── zh-CN.yml └── zh-TW.yml ├── main.py ├── privacy-policy.md ├── requirements.txt ├── src ├── settings.py ├── utils.py └── websites.py └── terms-of-service.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Kyrela 2 | -------------------------------------------------------------------------------- /.github/workflows/crowdin-download.yml: -------------------------------------------------------------------------------- 1 | name: Crowdin Download Action 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 0 * * *' 7 | 8 | jobs: 9 | synchronize-with-crowdin: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Download crowdin translations 17 | uses: crowdin/github-action@v2 18 | with: 19 | upload_sources: false 20 | upload_translations: false 21 | download_translations: true 22 | localization_branch_name: 'l10n' 23 | 24 | source: "locales/en-US.yml" 25 | translation: "locales/%two_letters_code%.yml" 26 | 27 | skip_untranslated_strings: true 28 | export_only_approved: true 29 | 30 | commit_message: "🌐 locales: new crowdin translations" 31 | 32 | create_pull_request: true 33 | pull_request_title: 'New Crowdin translations' 34 | pull_request_body: 'New Crowdin pull request with translations' 35 | pull_request_assignees: 'Kyrela' 36 | pull_request_base_branch_name: 'main' 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 40 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 41 | -------------------------------------------------------------------------------- /.github/workflows/crowdin-upload.yml: -------------------------------------------------------------------------------- 1 | name: Crowdin Upload Action 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths: [ 'locales/**' ] 7 | 8 | jobs: 9 | synchronize-with-crowdin: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Upload crowdin translations 17 | uses: crowdin/github-action@v2 18 | with: 19 | upload_sources: true 20 | upload_translations: true 21 | auto_approve_imported: true 22 | download_sources: false 23 | download_translations: false 24 | 25 | source: "locales/en-US.yml" 26 | translation: "locales/%two_letters_code%.yml" 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 29 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 30 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | version_check: 10 | runs-on: ubuntu-latest 11 | outputs: 12 | should_update: ${{ steps.outputs.outputs.should_update }} 13 | version: ${{ steps.outputs.outputs.version }} 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - name: Get version 20 | id: get_version 21 | run: | 22 | echo "version=$(grep -oP '(?<=version: ")[^"]*' config.yml)" >> $GITHUB_OUTPUT 23 | - name: Get latest version tag 24 | id: get_latest_tag 25 | uses: WyriHaximus/github-action-get-previous-tag@v1 26 | with: 27 | prefix: v 28 | fallback: 0.0.0 29 | - name: Compare versions 30 | id: compare_versions 31 | run: | 32 | echo "should_update=$(python3 -c "from packaging import version; print(version.parse('${{ steps.get_version.outputs.version }}') > version.parse('${{ steps.get_latest_tag.outputs.tag }}'))")" >> $GITHUB_OUTPUT 33 | - name: Set outputs 34 | id: outputs 35 | if: steps.compare_versions.outputs.should_update == 'True' 36 | run: | 37 | echo "should_update=True" >> $GITHUB_OUTPUT 38 | echo "version=${{ steps.get_version.outputs.version }}" >> $GITHUB_OUTPUT 39 | 40 | create_release: 41 | needs: version_check 42 | if: needs.version_check.outputs.should_update == 'True' 43 | runs-on: ubuntu-latest 44 | steps: 45 | - name: Checkout 46 | uses: actions/checkout@v4 47 | with: 48 | ref: main 49 | - name: Get Changelog 50 | id: get_changelog 51 | run: | 52 | { 53 | echo 'changelog<> "$GITHUB_OUTPUT" 58 | - name: Create release 59 | uses: softprops/action-gh-release@v2 60 | with: 61 | tag_name: v${{ needs.version_check.outputs.version }} 62 | body: ${{ steps.get_changelog.outputs.changelog }} 63 | - name: Send changelog to Discord 64 | uses: tsickert/discord-webhook@v5.3.0 65 | with: 66 | webhook-url: ${{ secrets.WEBHOOK_URL }} 67 | content: | 68 | # Version ${{ needs.version_check.outputs.version }} 69 | <@&${{ secrets.WEBHOOK_ROLE }}> 70 | 71 | ${{ steps.get_changelog.outputs.changelog }} 72 | 73 | publish_images: 74 | needs: [version_check, create_release] 75 | if: needs.version_check.outputs.should_update == 'True' 76 | runs-on: ubuntu-latest 77 | permissions: 78 | contents: read 79 | packages: write 80 | steps: 81 | - name: Checkout repository 82 | uses: actions/checkout@v4 83 | with: 84 | ref: main 85 | - name: Log in to Docker Hub 86 | uses: docker/login-action@v3 87 | with: 88 | username: ${{ secrets.DOCKERHUB_USERNAME }} 89 | password: ${{ secrets.DOCKERHUB_TOKEN }} 90 | - name: Log in to GitHub Container Registry 91 | uses: docker/login-action@v3 92 | with: 93 | registry: ghcr.io 94 | username: ${{ github.repository_owner }} 95 | password: ${{ secrets.GITHUB_TOKEN }} 96 | - name: Set up QEMU 97 | uses: docker/setup-qemu-action@v3 98 | - name: Set up Docker Buildx 99 | uses: docker/setup-buildx-action@v3 100 | - name: Extract Docker metadata 101 | id: meta 102 | uses: docker/metadata-action@v5 103 | with: 104 | images: | 105 | ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.repository }} 106 | ghcr.io/${{ github.repository }} 107 | tags: | 108 | type=raw,value=latest 109 | type=semver,pattern={{version}},value=v${{ needs.version_check.outputs.version }} 110 | type=semver,pattern={{version}},value=${{ needs.version_check.outputs.version }} 111 | type=semver,pattern={{major}}.{{minor}},value=v${{ needs.version_check.outputs.version }} 112 | type=semver,pattern={{major}}.{{minor}},value=${{ needs.version_check.outputs.version }} 113 | type=semver,pattern={{major}},value=v${{ needs.version_check.outputs.version }} 114 | type=semver,pattern={{major}},value=${{ needs.version_check.outputs.version }} 115 | - name: Build and push Docker image 116 | uses: docker/build-push-action@v5 117 | with: 118 | context: . 119 | push: true 120 | tags: ${{ steps.meta.outputs.tags }} 121 | labels: ${{ steps.meta.outputs.labels }} 122 | platforms: linux/amd64,linux/arm64,linux/arm/v7 123 | cache-from: type=gha 124 | cache-to: type=gha,mode=max 125 | 126 | deployment: 127 | needs: [version_check, create_release] 128 | if: needs.version_check.outputs.should_update == 'True' 129 | runs-on: self-hosted 130 | steps: 131 | - name: Stop 132 | run: | 133 | cd ${{ secrets.DIRECTORY }} 134 | pm2 stop FixTweetBot 135 | rm -f log.txt 136 | - name: Checkout 137 | run: | 138 | git fetch --all --tags 139 | git reset --hard 140 | git checkout refs/tags/v${{ needs.version_check.outputs.version }} 141 | - name: Install dependencies 142 | run: | 143 | venv/bin/pip install --force-reinstall -r requirements.txt 144 | - name: Migrate database 145 | run: | 146 | venv/bin/masonite-orm migrate -C database/config.py -d database/migrations 147 | - name: Start 148 | run: | 149 | pm2 start FixTweetBot 150 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | log.txt 2 | .env 3 | override.config.yml 4 | venv/ 5 | .idea/ 6 | .vscode/ 7 | .fleet/ 8 | __pycache__/ 9 | *.pyc 10 | core 11 | docker-compose.yml 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Step 1: Builder 2 | 3 | FROM python:3.13-alpine AS builder 4 | WORKDIR /usr/local/app 5 | 6 | COPY ./ ./ 7 | 8 | RUN apk add --no-cache git linux-headers cargo rust 9 | RUN python -m venv /usr/local/venv && . /usr/local/venv/bin/activate 10 | RUN /usr/local/venv/bin/pip install --no-cache-dir -r requirements.txt && \ 11 | /usr/local/venv/bin/pip install --no-cache-dir cryptography 12 | 13 | # Step 2: Final image 14 | 15 | FROM python:3.13-alpine 16 | WORKDIR /usr/local/app 17 | 18 | ENV USR=app 19 | ENV GRP=$USR 20 | ENV UID=1000 21 | ENV GID=1000 22 | 23 | ENV PATH="/usr/local/venv/bin:$PATH" 24 | ENV DISCORE_CONFIG=/usr/local/app/docker.config.yml 25 | ENV DATABASE_DRIVER="mysql" 26 | 27 | # needed for console logging 28 | ENV PYTHONUNBUFFERED=1 29 | ENV PYTHONDONTWRITEBYTECODE=1 30 | 31 | RUN apk add --no-cache netcat-openbsd 32 | 33 | RUN addgroup \ 34 | --gid "$GID" \ 35 | $GRP \ 36 | && \ 37 | adduser \ 38 | --disabled-password \ 39 | --no-create-home \ 40 | --home "$(pwd)" \ 41 | --uid "$UID" \ 42 | --ingroup "$GRP" \ 43 | $USR 44 | 45 | COPY --from=builder /usr/local/venv /usr/local/venv 46 | COPY --from=builder /usr/local/app /usr/local/app 47 | 48 | RUN chown -R $USR:$GRP . 49 | 50 | COPY docker-entrypoint.sh / 51 | RUN chmod +x /docker-entrypoint.sh 52 | 53 | USER $USR 54 | 55 | ENTRYPOINT ["/docker-entrypoint.sh"] 56 | CMD ["python", "main.py"] 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Kyrela 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 | 23 | “Commons Clause” License Condition v1.0 24 | 25 | The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition. 26 | 27 | Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, the right to Sell the Software. 28 | 29 | For purposes of the foregoing, “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/ support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any license notice or attribution required by the License must also include this Commons Clause License Condition notice. 30 | 31 | Software: FixTweetBot 32 | 33 | License: MIT 34 | 35 | Licensor: Kyrela 36 | -------------------------------------------------------------------------------- /assets/FixTweetDemo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/FixTweetDemo.mp4 -------------------------------------------------------------------------------- /assets/apple_music.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/apple_music.webp -------------------------------------------------------------------------------- /assets/bilibili.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/bilibili.webp -------------------------------------------------------------------------------- /assets/bluesky.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/bluesky.webp -------------------------------------------------------------------------------- /assets/deviantart.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/deviantart.webp -------------------------------------------------------------------------------- /assets/facebook.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/facebook.webp -------------------------------------------------------------------------------- /assets/furaffinity.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/furaffinity.webp -------------------------------------------------------------------------------- /assets/ifunny.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/ifunny.webp -------------------------------------------------------------------------------- /assets/instagram.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/instagram.webp -------------------------------------------------------------------------------- /assets/logo.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/logo.pdn -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/logo.png -------------------------------------------------------------------------------- /assets/logo_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/logo_alpha.png -------------------------------------------------------------------------------- /assets/logo_dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/logo_dev.png -------------------------------------------------------------------------------- /assets/logo_rounded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/logo_rounded.png -------------------------------------------------------------------------------- /assets/mastodon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/mastodon.webp -------------------------------------------------------------------------------- /assets/newgrounds.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/newgrounds.webp -------------------------------------------------------------------------------- /assets/nitter.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/nitter.webp -------------------------------------------------------------------------------- /assets/pixiv.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/pixiv.webp -------------------------------------------------------------------------------- /assets/reddit.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/reddit.webp -------------------------------------------------------------------------------- /assets/settings.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/settings.gif -------------------------------------------------------------------------------- /assets/showcase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/showcase.png -------------------------------------------------------------------------------- /assets/snapchat.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/snapchat.webp -------------------------------------------------------------------------------- /assets/spotify.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/spotify.webp -------------------------------------------------------------------------------- /assets/threads.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/threads.webp -------------------------------------------------------------------------------- /assets/tiktok.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/tiktok.webp -------------------------------------------------------------------------------- /assets/tumblr.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/tumblr.webp -------------------------------------------------------------------------------- /assets/twitch.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/twitch.webp -------------------------------------------------------------------------------- /assets/twitter.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/twitter.webp -------------------------------------------------------------------------------- /assets/usage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/usage.png -------------------------------------------------------------------------------- /assets/youtube.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/youtube.webp -------------------------------------------------------------------------------- /assets/youtube_music.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/youtube_music.webp -------------------------------------------------------------------------------- /assets/youtube_shorts.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyrela/FixTweetBot/877a51af3f968d09ff224ca1ed58e4efdb86d2df/assets/youtube_shorts.webp -------------------------------------------------------------------------------- /cogs/commands.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | 5 | import discore 6 | 7 | from src.utils import * 8 | from src.settings import SettingsView 9 | 10 | __all__ = ('Commands',) 11 | 12 | _logger = logging.getLogger(__name__) 13 | 14 | 15 | class Commands(discore.Cog, 16 | name="commands", 17 | description="The bot commands"): 18 | 19 | def __init__(self, *args, **kwargs): 20 | if not discore.config.about_command: 21 | self.__cog_app_commands__.remove(self.about) 22 | del self.about 23 | _logger.info("Disabling about command") 24 | super().__init__(*args, **kwargs) 25 | 26 | 27 | @discore.app_commands.command( 28 | name=tstr('settings.command.name'), 29 | description=tstr('settings.command.description')) 30 | @discore.app_commands.guild_only() 31 | @discore.app_commands.default_permissions(manage_messages=True) 32 | @discore.app_commands.rename( 33 | channel=tstr('settings.command.channel.name'), 34 | member=tstr('settings.command.member.name'), 35 | role=tstr('settings.command.role.name') 36 | ) 37 | @discore.app_commands.describe( 38 | channel=tstr('settings.command.channel.description'), 39 | member=tstr('settings.command.member.description'), 40 | role=tstr('settings.command.role.description') 41 | ) 42 | async def settings( 43 | self, 44 | i: discore.Interaction, 45 | channel: Optional[discore.TextChannel | discore.Thread] = None, 46 | member: Optional[discore.Member] = None, 47 | role: Optional[discore.Role] = None, 48 | ): 49 | await SettingsView(i, channel or i.channel, member or i.user, role or i.user.top_role).send(i) 50 | 51 | @discore.app_commands.command( 52 | name=tstr('about.command.name'), 53 | description=tstr('about.command.description')) 54 | @discore.app_commands.guild_only() 55 | async def about(self, i: discore.Interaction): 56 | embed = discore.Embed( 57 | title=t('about.name'), 58 | description=t('about.description')) 59 | discore.set_embed_footer(self.bot, embed) 60 | embed.add_field( 61 | name=t('about.help.name'), 62 | value=t('about.help.value'), 63 | inline=False 64 | ) 65 | embed.add_field( 66 | name=t('about.premium.name'), 67 | value=t(f'about.premium.{str(bool(is_premium(i))).lower()}'), 68 | inline=False) 69 | embed.add_field( 70 | name=t('about.links.name'), 71 | value=t('about.links.value', 72 | invite_link=discore.config.invite_link.format(id=self.bot.application_id), 73 | support_link=discore.config.support_link, 74 | repo_link=discore.config.repo_link), 75 | inline=False) 76 | view = discore.ui.View() 77 | if not is_premium(i) and is_sku(): 78 | view.add_item(discore.ui.Button( 79 | style=discore.ButtonStyle.premium, 80 | sku_id=discore.config.sku 81 | )) 82 | view.add_item(discore.ui.Button( 83 | style=discore.ButtonStyle.link, 84 | label=t('about.source'), 85 | url=discore.config.repo_link, 86 | emoji=discore.PartialEmoji.from_str(discore.config.emoji.github) 87 | )) 88 | view.add_item(discore.ui.Button( 89 | style=discore.ButtonStyle.link, 90 | label=t('about.invite'), 91 | url=discore.config.invite_link.format(id=self.bot.application_id), 92 | emoji=discore.PartialEmoji.from_str(discore.config.emoji.add) 93 | )) 94 | view.add_item(discore.ui.Button( 95 | style=discore.ButtonStyle.link, 96 | label=t('about.support'), 97 | url=discore.config.support_link, 98 | emoji=discore.PartialEmoji.from_str(discore.config.emoji.discord) 99 | )) 100 | 101 | # Discord API sometimes returns incorrect error code, in this case 404 Unknown interaction when interaction 102 | # is actually found and the message has been sent. 103 | # Even if the interaction is really not found, there's not much we can do (as the interaction is lost), 104 | # so in both cases we just ignore the error. 105 | try: 106 | await i.response.send_message(embed=embed, view=view) 107 | except discore.NotFound: 108 | pass 109 | -------------------------------------------------------------------------------- /cogs/developer.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import subprocess 4 | import sys 5 | from importlib import metadata 6 | 7 | import psutil 8 | from textwrap import shorten 9 | from typing import Optional 10 | 11 | import discore 12 | 13 | __all__ = ('Developer',) 14 | 15 | import requests 16 | 17 | p = psutil.Process() 18 | p.cpu_percent() 19 | 20 | dev_guilds = [discore.config.dev_guild] if discore.config.dev_guild else [] 21 | 22 | 23 | def execute_command(command: str, timeout: int = 30) -> str: 24 | """ 25 | Execute a shell command 26 | :param command: the command to execute 27 | :param timeout: the timeout of the command 28 | :return: the output of the command 29 | """ 30 | 31 | try: 32 | output = subprocess.Popen( 33 | command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).communicate(timeout=timeout) 34 | except subprocess.TimeoutExpired: 35 | return "Command expired" 36 | try: 37 | res = [] 38 | if stdout := output[0].decode('utf-8'): 39 | res.append(stdout) 40 | if stderr := output[1].decode('utf-8'): 41 | res.append(stderr) 42 | res = '\n'.join(res).replace("```", "'''").replace("\n", "¶") 43 | sanitized_output = shorten(res, width=1992, placeholder='...').replace("¶", "\n") 44 | except: 45 | return "Displaying the command result is impossible" 46 | return f"```\n{sanitized_output}\n```" if sanitized_output else "Command executed correctly" 47 | 48 | 49 | class Developer(discore.Cog, 50 | name="developer", 51 | description="The bot commands"): 52 | 53 | @discore.app_commands.command( 54 | name="update", 55 | description="Update the bot", 56 | auto_locale_strings=False) 57 | @discore.app_commands.guilds(*dev_guilds) 58 | async def update(self, i: discore.Interaction) -> None: 59 | await i.response.defer(thinking=True) 60 | await i.followup.send(execute_command("git pull")) 61 | 62 | @discore.app_commands.command( 63 | name="requirements", 64 | description="Update the bot requirements", 65 | auto_locale_strings=False) 66 | @discore.app_commands.guilds(*dev_guilds) 67 | async def requirements(self, i: discore.Interaction) -> None: 68 | await i.response.defer(thinking=True) 69 | await i.followup.send(execute_command("pip install --force-reinstall -r requirements.txt", timeout=120)) 70 | 71 | @discore.app_commands.command( 72 | name="shell", 73 | description="Execute a shell command", 74 | auto_locale_strings=False) 75 | @discore.app_commands.guilds(*dev_guilds) 76 | async def shell(self, i: discore.Interaction, command: str, timeout: Optional[int] = 30) -> None: 77 | await i.response.defer(thinking=True) 78 | await i.followup.send(execute_command(command, timeout=timeout)) 79 | 80 | @discore.app_commands.command( 81 | name="exec", 82 | description="Execute python code", 83 | auto_locale_strings=False) 84 | @discore.app_commands.guilds(*dev_guilds) 85 | async def _exec(self, i: discore.Interaction, code: str) -> None: 86 | await i.response.defer(thinking=True) 87 | code_lines = code.split('\n') 88 | if len(code_lines) == 1 and ";" not in code: 89 | code_lines[0] = f"return {code}" 90 | code = '\n '.join(code_lines) 91 | function = f'async def _ex(self, i):\n {code}' 92 | try: 93 | exec(function) 94 | res = repr(await locals()["_ex"](self, i)) 95 | except Exception as e: 96 | await i.followup.send(f"```{e}```") 97 | return 98 | await i.followup.send( 99 | f"```py\n{discore.utils.sanitize(res, limit=1990)}\n```") 100 | 101 | @discore.app_commands.command( 102 | name="log", 103 | description="Get the bot log", 104 | auto_locale_strings=False) 105 | @discore.app_commands.guilds(*dev_guilds) 106 | async def log(self, i: discore.Interaction) -> None: 107 | with open(discore.config.log.file, encoding='utf-8') as f: 108 | logs = f.read() 109 | await i.response.send_message( 110 | f"```\n{discore.sanitize(logs, 1992, replace_newline=False, crop_at_end=False)}\n```") 111 | 112 | @discore.app_commands.command( 113 | name="runtime", 114 | description="Get the bot runtime", 115 | auto_locale_strings=False) 116 | @discore.app_commands.guilds(*dev_guilds) 117 | async def runtime(self, i: discore.Interaction) -> None: 118 | global p 119 | 120 | direct_url = json.loads([p for p in metadata.files('discore') if 'direct_url.json' in str(p)][0].read_text()) 121 | author, repo = direct_url["url"].removeprefix("https://github.com/").split("/") 122 | discore_commit = direct_url["vcs_info"]["commit_id"] 123 | discore_commit_api_url = f"https://api.github.com/repos/{author}/{repo}/commits/{discore_commit}" 124 | raw_commit_date = requests.get(discore_commit_api_url).json()["commit"]["committer"]["date"] 125 | datetime_commit_date = datetime.datetime.strptime( 126 | raw_commit_date, "%Y-%m-%dT%H:%M:%SZ") + datetime.timedelta(hours=2) 127 | commit_date = int(datetime_commit_date.timestamp()) 128 | 129 | e = discore.Embed( 130 | title="Runtime", 131 | color=discore.config.color or None) 132 | e.add_field( 133 | name="Bot Version", 134 | value=discore.config.version) 135 | e.add_field( 136 | name="Bot commit", 137 | value='`' + subprocess.check_output('git log -1 --pretty=format:"%h"', shell=True, encoding='utf-8') + '`') 138 | e.add_field( 139 | name="Bot commit date", 140 | value="") 142 | e.add_field( 143 | name="Discore Version", 144 | value=metadata.version('discore')) 145 | e.add_field( 146 | name="Discore commit", 147 | value=f"`{discore_commit[:7]}`") 148 | e.add_field( 149 | name="Discore commit date", 150 | value=f"") 151 | e.add_field( 152 | name="Discord.py Version", 153 | value=metadata.version('discord.py')) 154 | e.add_field( 155 | name="Python Version", 156 | value='.'.join(map(str, sys.version_info[:3]))) 157 | e.add_field( 158 | name="Platform", 159 | value=sys.platform) 160 | e.add_field( 161 | name="Start time", 162 | value=f"" 163 | if self.bot.start_time 164 | else f" (init)") 165 | e.add_field( 166 | name="Uptime", 167 | value=f"" 168 | if self.bot.start_time 169 | else f" (init)") 170 | e.add_field( 171 | name="Current time", 172 | value=f"") 173 | e.add_field( 174 | name="Memory usage", 175 | value=f"{p.memory_info().rss / 1024 / 1024:.2f} MB") 176 | e.add_field( 177 | name="CPU usage", 178 | value=f"{p.cpu_percent():.2f} %") 179 | e.set_footer( 180 | text=self.bot.user.name + ( 181 | f" | ver. {discore.config.version}" if discore.config.version else ""), 182 | icon_url=self.bot.user.display_avatar.url 183 | ) 184 | discore.set_embed_footer(self.bot, e) 185 | 186 | await i.response.send_message(embed=e) 187 | 188 | 189 | @discore.app_commands.command( 190 | name="add_premium", 191 | description="Enable the premium features to test", 192 | auto_locale_strings=False) 193 | @discore.app_commands.guilds(*dev_guilds) 194 | async def add_premium(self, i: discore.Interaction) -> None: 195 | if not discore.config.sku: 196 | await i.response.send_message("SKU not set") 197 | return 198 | await self.bot.create_entitlement( 199 | sku=discore.Object(id=discore.config.sku), owner=i.guild, owner_type=discore.EntitlementOwnerType.guild) 200 | await i.response.send_message("Premium enabled") 201 | 202 | @discore.app_commands.command( 203 | name="remove_premium", 204 | description="Disable the premium features", 205 | auto_locale_strings=False) 206 | @discore.app_commands.guilds(*dev_guilds) 207 | async def remove_premium(self, i: discore.Interaction) -> None: 208 | if not discore.config.sku: 209 | await i.response.send_message("SKU not set") 210 | return 211 | for entitlement in i.entitlements: 212 | if entitlement.sku_id == discore.config.sku: 213 | try: 214 | await entitlement.delete() 215 | except: 216 | await i.response.send_message("Failed to disable premium") 217 | await i.response.send_message("Premium disabled") 218 | -------------------------------------------------------------------------------- /cogs/link_fix.py: -------------------------------------------------------------------------------- 1 | """ 2 | Intercepts messages, detects links that can be fixed, and sends the fixed links accordingly. 3 | """ 4 | 5 | import asyncio 6 | from typing import List 7 | import discord_markdown_ast_parser as dmap 8 | from discord_markdown_ast_parser.parser import NodeType 9 | import logging 10 | 11 | from database.models.Member import * 12 | from database.models.Role import Role 13 | from database.models.TextChannel import * 14 | from database.models.Guild import * 15 | from database.models.Event import * 16 | from src.websites import * 17 | from src.utils import * 18 | 19 | import discore 20 | 21 | __all__ = ('LinkFix',) 22 | 23 | _logger = logging.getLogger(__name__) 24 | 25 | 26 | def get_website(guild: Guild, url: str) -> Optional[WebsiteLink]: 27 | """ 28 | Get the website of the URL. 29 | 30 | :param guild: the guild associated with the context 31 | :param url: the URL to check 32 | :return: the website of the URL 33 | """ 34 | 35 | for website in websites: 36 | if link := website.if_valid(guild, url): 37 | return link 38 | return None 39 | 40 | 41 | def filter_fixable_links(links: List[tuple[str, bool]], guild: Guild) -> List[tuple[WebsiteLink, bool]]: 42 | """ 43 | Get only the fixable links from the list of links. 44 | 45 | :param links: the links to filter (url, spoiler) 46 | :param guild: the guild associated with the context 47 | :return: the fixable links, as WebsiteLink, along with their spoiler status 48 | """ 49 | 50 | return [(link, spoiler) for url, spoiler in links if (link := get_website(guild, url))] 51 | 52 | 53 | def get_embeddable_urls(nodes: List[dmap.Node], spoiler: bool = False) -> List[tuple[str, bool]]: 54 | """ 55 | Parse and detects the embeddable links, ignoring links 56 | that are in a code block, in spoiler or ignored with <> 57 | 58 | :param nodes: the list of nodes to parse 59 | :param spoiler: if the nodes are in a spoiler 60 | :return: the list of detected links (url, spoiler) 61 | """ 62 | 63 | links = [] 64 | for node in nodes: 65 | match node.node_type: 66 | case NodeType.CODE_BLOCK | NodeType.CODE_INLINE: 67 | continue 68 | case NodeType.URL_WITH_PREVIEW_EMBEDDED | NodeType.URL_WITH_PREVIEW: 69 | links.append((node.url, spoiler)) 70 | case NodeType.SPOILER: 71 | links += get_embeddable_urls(node.children, spoiler=True) 72 | case _: 73 | links += get_embeddable_urls(node.children, spoiler=spoiler) 74 | return links 75 | 76 | 77 | async def fix_embeds( 78 | message: discore.Message, 79 | guild: Guild, 80 | links: List[tuple[WebsiteLink, bool]]) -> None: 81 | """ 82 | Edit the message if necessary, and send the fixed links. 83 | 84 | :param message: the message to fix 85 | :param guild: the guild associated with the context 86 | :param links: the matches to fix 87 | :return: None 88 | """ 89 | 90 | permissions = message.channel.permissions_for(message.guild.me) 91 | 92 | if not permissions.send_messages or not permissions.embed_links: 93 | return 94 | 95 | async with message.channel.typing(): 96 | fixed_links = [] 97 | for link, spoiler in links: 98 | fixed_link = await link.render() 99 | if not fixed_link: 100 | continue 101 | if spoiler: 102 | fixed_link = f"||{fixed_link} ||" 103 | fixed_links.append(fixed_link) 104 | if discore.config.analytic: 105 | Event.create({'name': 'link_' + link.id}) 106 | 107 | if not fixed_links: 108 | return 109 | 110 | await send_fixed_links(fixed_links, guild, message) 111 | 112 | await edit_original_message(guild, message, permissions) 113 | 114 | 115 | async def send_fixed_links(fixed_links: list[str], guild: Guild, original_message: discore.Message) -> None: 116 | """ 117 | Send the fixed links to the channel, according to the guild settings and its context 118 | 119 | :param fixed_links: the fixed links to send, as strings 120 | :param guild: the guild associated with the context 121 | :param original_message: the original message associated with the context to reply to 122 | :return: None 123 | """ 124 | 125 | messages = group_join(fixed_links, 2000) 126 | 127 | if guild.reply_to_message: 128 | await discore.fallback_reply(original_message, messages.pop(0), silent=guild.reply_silently) 129 | 130 | for message in messages: 131 | await original_message.channel.send(message, silent=guild.reply_silently) 132 | 133 | 134 | async def edit_original_message(guild: Guild, message: discore.Message, permissions: discore.Permissions) -> None: 135 | """ 136 | Edit the original message according to the guild settings and permissions. 137 | 138 | :param guild: the guild associated with the context 139 | :param message: the message to edit 140 | :param permissions: the permissions of the bot in the channel the message was sent in 141 | :return: None 142 | """ 143 | if permissions.manage_messages and guild.original_message != OriginalMessage.NOTHING: 144 | try: 145 | if guild.original_message == OriginalMessage.DELETE: 146 | await message.delete() 147 | else: 148 | await message.edit(suppress=True) 149 | await asyncio.sleep(2) 150 | await message.edit(suppress=True) 151 | except discore.NotFound: 152 | pass 153 | 154 | 155 | class LinkFix(discore.Cog, 156 | name="link fix", 157 | description="Link fix events"): 158 | 159 | @discore.Cog.listener() 160 | async def on_message(self, message: discore.Message) -> None: 161 | """ 162 | React to message creation events 163 | 164 | :param message: The message that was created 165 | :return: None 166 | """ 167 | 168 | if ( 169 | message.author == message.guild.me 170 | or not message.content 171 | or not message.channel 172 | or not message.guild 173 | or message.is_system() 174 | ): 175 | return 176 | 177 | urls = get_embeddable_urls(dmap.parse(message.content)) 178 | 179 | if not urls: 180 | return 181 | 182 | guild = Guild.find_or_create(message.guild.id) 183 | links = filter_fixable_links(urls, guild) 184 | 185 | if not links: 186 | return 187 | if not TextChannel.find_or_create(guild, message.channel.id).enabled: 188 | return 189 | if isinstance(message.author, discore.Member) and ( 190 | not Member.find_or_create(message.author, guild).enabled 191 | or not all(r.enabled for r in Role.finds_or_creates(guild, [role.id for role in message.author.roles])) 192 | ): 193 | return 194 | if message.webhook_id is not None and not bool(guild.webhooks): 195 | return 196 | 197 | await fix_embeds(message, guild, links) 198 | -------------------------------------------------------------------------------- /cogs/setup.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import topgg 3 | 4 | from src.utils import is_sku 5 | from database.models.Event import * 6 | 7 | import discore 8 | 9 | __all__ = ('Setup',) 10 | 11 | _logger = logging.getLogger(__name__) 12 | 13 | 14 | class Setup(discore.Cog, 15 | name="setup", 16 | description="The bot setup, such as guild sync and top.gg autopost"): 17 | 18 | def __init__(self, *args, **kwargs): 19 | super().__init__(*args, **kwargs) 20 | 21 | self.topgg_client = None 22 | if discore.config.topgg_token: 23 | self.topgg_client = topgg.DBLClient(discore.config.topgg_token) 24 | 25 | @discore.Cog.listener() 26 | async def on_connect(self): 27 | if discore.config.analytic and not self.update_activity.is_running(): 28 | _logger.info("[ACTIVITY] Starting custom activity") 29 | self.update_activity.start() 30 | elif not discore.config.analytic: 31 | _logger.warning("[ACTIVITY] Analytics disabled, activity disabled") 32 | 33 | if self.topgg_client and not self.topgg_autopost.is_running(): 34 | _logger.info("[TOP.GG] Starting autopost") 35 | self.topgg_autopost.start() 36 | elif not self.topgg_client: 37 | _logger.warning("[TOP.GG] `config.topgg_token` not set, autopost disabled") 38 | 39 | async def cog_unload(self): 40 | if self.update_activity.is_running(): 41 | self.update_activity.cancel() 42 | 43 | if self.topgg_autopost.is_running(): 44 | self.topgg_autopost.cancel() 45 | 46 | @discore.Cog.listener() 47 | async def on_login(self): 48 | if discore.config.dev_guild and discore.config.auto_sync: 49 | await self.bot.tree.sync(guild=discore.Object(discore.config.dev_guild)) 50 | _logger.info("Synced dev guild") 51 | else: 52 | _logger.warning("`config.dev_guild` not set, skipping dev commands sync") 53 | 54 | if not is_sku(): 55 | _logger.warning("`config.sku` not set, premium features unavailable") 56 | 57 | @discore.loop(hours=1) 58 | async def update_activity(self) -> None: 59 | """ 60 | Update the bot activity every hour. 61 | 62 | :return: None 63 | """ 64 | 65 | if self.bot.shard_count is None: 66 | _logger.warning("[ACTIVITY] Websocket not connected, skipping activity update") 67 | return 68 | 69 | fixed_links_nb = len(Event.since()) 70 | if fixed_links_nb == 0: 71 | return 72 | 73 | activity = discore.CustomActivity(f"Fixing {fixed_links_nb} links per day") 74 | _logger.info(f"[ACTIVITY] {activity}") 75 | await self.bot.change_presence(activity=activity) 76 | 77 | @discore.loop(hours=1) 78 | async def topgg_autopost(self) -> None: 79 | """ 80 | Update the guild count on top.gg every hour. 81 | 82 | :return: None 83 | """ 84 | 85 | guild_count = len(self.bot.guilds) 86 | if guild_count == 0: 87 | _logger.warning("[TOP.GG] No guilds found, skipping autopost") 88 | return 89 | 90 | await self.topgg_client.post_guild_count(guild_count=guild_count) 91 | _logger.info("[TOP.GG] Updated guild count") 92 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | description: "This bot automatically repost x.com and twitter.com posts as fxtwitter ones." 2 | version: "3.2.9" 3 | color: 0x1d9bf0 4 | hot_reload: false 5 | about_command: true 6 | analytic: true 7 | 8 | log: 9 | file: "log.txt" 10 | alert_user: false 11 | stream_to_err: false 12 | date_format: "%d/%m/%Y %H:%M:%S" 13 | format: "[{asctime}] {levelformat} {name:<20} {message}" 14 | 15 | emoji: 16 | github: "🖥️" 17 | add: "➕" 18 | discord: "💬" 19 | twitter: "🐦" 20 | instagram: "📸" 21 | tiktok: "🎶" 22 | reddit: "🤖" 23 | threads: "🧵" 24 | snapchat: "👻" 25 | facebook: "📘" 26 | bluesky: "🦋" 27 | pixiv: "🖼️" 28 | mastodon: "🐘" 29 | tumblr: "📝" 30 | deviantart: "🎨" 31 | twitch: "🎮" 32 | spotify: "🎵" 33 | bilibili: "📺" 34 | ifunny: "🙂" 35 | furaffinity: "🐾" 36 | youtube: "📺" 37 | reply: "↩️" 38 | webhooks: "🔄" 39 | x: "❎" 40 | fixtweet: "🤖" 41 | role: "🎭" 42 | 43 | invite_link: "https://discord.com/oauth2/authorize?client_id={id}" 44 | repo_link: "https://github.com/Kyrela/FixTweetBot" 45 | support_link: "https://discord.gg/3ej9JrkF3U" 46 | -------------------------------------------------------------------------------- /database/config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import discore 4 | 5 | from masoniteorm.connections import ConnectionResolver 6 | 7 | if not discore.config.loaded: 8 | discore.config_init() 9 | discore.logging_init() 10 | 11 | DB = ConnectionResolver().set_connection_details({ 12 | "default": "main", 13 | "main": discore.config.database 14 | }) 15 | 16 | -------------------------------------------------------------------------------- /database/migrations/2023_11_17_092808_guilds_table.py: -------------------------------------------------------------------------------- 1 | """GuildsTable Migration.""" 2 | 3 | from masoniteorm.migrations import Migration 4 | 5 | 6 | class GuildsTable(Migration): 7 | def up(self): 8 | """ 9 | Run the migrations. 10 | """ 11 | with self.schema.create("guilds") as table: 12 | table.big_integer("id").unsigned().unique() 13 | 14 | table.timestamps() 15 | 16 | def down(self): 17 | """ 18 | Revert the migrations. 19 | """ 20 | self.schema.drop("guilds") 21 | -------------------------------------------------------------------------------- /database/migrations/2023_11_17_094206_text_channels_table.py: -------------------------------------------------------------------------------- 1 | """TextChannelsTable Migration.""" 2 | 3 | from masoniteorm.migrations import Migration 4 | 5 | 6 | class TextChannelsTable(Migration): 7 | def up(self): 8 | """ 9 | Run the migrations. 10 | """ 11 | with self.schema.create("text_channels") as table: 12 | table.big_integer("id").unsigned().unique() 13 | 14 | table.big_integer("guild_id").unsigned().index() 15 | table.foreign("guild_id").references("id").on("guilds").on_delete("cascade") 16 | 17 | table.boolean("fix_twitter").default(True) 18 | 19 | table.timestamps() 20 | 21 | def down(self): 22 | """ 23 | Revert the migrations. 24 | """ 25 | self.schema.drop("text_channels") 26 | -------------------------------------------------------------------------------- /database/migrations/2023_11_19_180926_import_json.py: -------------------------------------------------------------------------------- 1 | """ImportJson Migration.""" 2 | 3 | from masoniteorm.migrations import Migration 4 | from masoniteorm.query import QueryBuilder 5 | 6 | import json 7 | from os import path 8 | 9 | 10 | class ImportJson(Migration): 11 | def up(self): 12 | """ 13 | Run the migrations. 14 | """ 15 | 16 | if not path.exists('db.json'): 17 | return 18 | 19 | guilds = QueryBuilder().on(self.connection).table('guilds') 20 | text_channels = QueryBuilder().on(self.connection).table('text_channels') 21 | 22 | with open('db.json', encoding='utf-8') as f: 23 | data = json.load(f) 24 | 25 | for guild_id, guild_content in data["guilds"].items(): 26 | guilds.create({ 27 | "id": guild_id 28 | }) 29 | for channel_id, channel_content in guild_content["channels"].items(): 30 | text_channels.create({ 31 | "id": channel_id, 32 | "guild_id": guild_id, 33 | "fix_twitter": channel_content["fixtweet"] 34 | }) 35 | 36 | def down(self): 37 | """ 38 | Revert the migrations. 39 | """ 40 | return 41 | 42 | -------------------------------------------------------------------------------- /database/migrations/2024_06_19_182807_guild_settings.py: -------------------------------------------------------------------------------- 1 | """GuildSettings Migration.""" 2 | 3 | from masoniteorm.migrations import Migration 4 | 5 | 6 | class GuildSettings(Migration): 7 | def up(self): 8 | """ 9 | Run the migrations. 10 | """ 11 | with self.schema.table("guilds") as table: 12 | table.enum("original_message", ["nothing", "remove_embeds", "delete"]).default("remove_embeds") 13 | table.boolean("reply").default(False) 14 | 15 | def down(self): 16 | """ 17 | Revert the migrations. 18 | """ 19 | with self.schema.table("guilds") as table: 20 | table.drop_column("original_message") 21 | table.drop_column("reply") 22 | -------------------------------------------------------------------------------- /database/migrations/2024_06_22_154629_channels_enabled.py: -------------------------------------------------------------------------------- 1 | """ChannelsEnabled Migration.""" 2 | 3 | from masoniteorm.migrations import Migration 4 | 5 | 6 | class ChannelsEnabled(Migration): 7 | def up(self): 8 | """ 9 | Run the migrations. 10 | """ 11 | with self.schema.table("text_channels") as table: 12 | table.rename('fix_twitter', 'enabled', 'boolean') 13 | table.boolean('enabled').default(True).change() 14 | 15 | def down(self): 16 | """ 17 | Revert the migrations. 18 | """ 19 | with self.schema.table("text_channels") as table: 20 | table.rename('enabled', 'fix_twitter', 'boolean') 21 | table.boolean('fix_twitter').default(True).change() 22 | -------------------------------------------------------------------------------- /database/migrations/2024_06_22_155714_websites_option.py: -------------------------------------------------------------------------------- 1 | """WebsitesOption Migration.""" 2 | 3 | from masoniteorm.migrations import Migration 4 | 5 | 6 | class WebsitesOption(Migration): 7 | def up(self): 8 | """ 9 | Run the migrations. 10 | """ 11 | with self.schema.table("guilds") as table: 12 | table.boolean('twitter').default(True) 13 | table.boolean('twitter_tr').default(False) 14 | table.string('twitter_tr_lang').nullable() 15 | table.boolean('instagram').default(True) 16 | 17 | def down(self): 18 | """ 19 | Revert the migrations. 20 | """ 21 | with self.schema.table("guilds") as table: 22 | table.drop_column('twitter') 23 | table.drop_column('twitter_tr') 24 | table.drop_column('twitter_tr_lang') 25 | table.drop_column('instagram') 26 | -------------------------------------------------------------------------------- /database/migrations/2024_06_24_175057_members_table.py: -------------------------------------------------------------------------------- 1 | """MembersTable Migration.""" 2 | 3 | from masoniteorm.migrations import Migration 4 | 5 | 6 | class MembersTable(Migration): 7 | def up(self): 8 | """ 9 | Run the migrations. 10 | """ 11 | with self.schema.create("members") as table: 12 | table.big_integer("id").unsigned().unique() 13 | 14 | table.big_integer("guild_id").unsigned().index() 15 | table.foreign("guild_id").references("id").on("guilds").on_delete("cascade") 16 | 17 | table.boolean("enabled").default(True) 18 | 19 | table.timestamps() 20 | 21 | def down(self): 22 | """ 23 | Revert the migrations. 24 | """ 25 | self.schema.drop("members") 26 | -------------------------------------------------------------------------------- /database/migrations/2024_06_25_131246_custom_websites_table.py: -------------------------------------------------------------------------------- 1 | """CustomWebsitesTable Migration.""" 2 | 3 | from masoniteorm.migrations import Migration 4 | 5 | 6 | class CustomWebsitesTable(Migration): 7 | def up(self): 8 | """ 9 | Run the migrations. 10 | """ 11 | with self.schema.create("custom_websites") as table: 12 | table.increments("id") 13 | 14 | table.big_integer("guild_id").unsigned().index() 15 | table.foreign("guild_id").references("id").on("guilds").on_delete("cascade") 16 | 17 | table.string("name") 18 | table.string("domain") 19 | table.string("fix_domain") 20 | 21 | table.timestamps() 22 | 23 | def down(self): 24 | """ 25 | Revert the migrations. 26 | """ 27 | self.schema.drop("custom_websites") 28 | -------------------------------------------------------------------------------- /database/migrations/2024_08_29_005648_default_states.py: -------------------------------------------------------------------------------- 1 | """DefaultStates Migration.""" 2 | 3 | from masoniteorm.migrations import Migration 4 | 5 | 6 | class DefaultStates(Migration): 7 | def up(self): 8 | """ 9 | Run the migrations. 10 | """ 11 | with self.schema.table("guilds") as table: 12 | table.boolean("default_channel_state").default(True) 13 | table.boolean("default_member_state").default(True) 14 | 15 | def down(self): 16 | """ 17 | Revert the migrations. 18 | """ 19 | with self.schema.table("guilds") as table: 20 | table.drop_column("default_channel_state") 21 | table.drop_column("default_member_state") 22 | -------------------------------------------------------------------------------- /database/migrations/2024_08_30_133515_roles_table.py: -------------------------------------------------------------------------------- 1 | """RolesTable Migration.""" 2 | 3 | from masoniteorm.migrations import Migration 4 | 5 | 6 | class RolesTable(Migration): 7 | def up(self): 8 | """ 9 | Run the migrations. 10 | """ 11 | with self.schema.create("roles") as table: 12 | table.big_integer("id").unsigned().unique() 13 | 14 | table.big_integer("guild_id").unsigned().index() 15 | table.foreign("guild_id").references("id").on("guilds").on_delete("cascade") 16 | 17 | table.boolean("enabled").default(True) 18 | 19 | table.timestamps() 20 | 21 | def down(self): 22 | """ 23 | Revert the migrations. 24 | """ 25 | self.schema.drop("roles") 26 | -------------------------------------------------------------------------------- /database/migrations/2024_08_30_143125_default_role_state.py: -------------------------------------------------------------------------------- 1 | """DefaultRoleState Migration.""" 2 | 3 | from masoniteorm.migrations import Migration 4 | 5 | 6 | class DefaultRoleState(Migration): 7 | def up(self): 8 | """ 9 | Run the migrations. 10 | """ 11 | with self.schema.table("guilds") as table: 12 | table.boolean("default_role_state").default(True) 13 | 14 | def down(self): 15 | """ 16 | Revert the migrations. 17 | """ 18 | with self.schema.table("guilds") as table: 19 | table.drop_column("default_role_state") 20 | -------------------------------------------------------------------------------- /database/migrations/2024_09_02_143111_websites_and_views.py: -------------------------------------------------------------------------------- 1 | """WebsitesAndViews Migration.""" 2 | 3 | from masoniteorm.migrations import Migration 4 | 5 | 6 | class WebsitesAndViews(Migration): 7 | def up(self): 8 | """ 9 | Run the migrations. 10 | """ 11 | with self.schema.table("guilds") as table: 12 | table.enum("instagram_view", ["normal", "gallery", "direct_media"]).default("normal") 13 | table.enum("twitter_view", ["normal", "gallery", "text_only", "direct_media"]).default("normal") 14 | table.boolean('tiktok').default(True) 15 | table.enum('tiktok_view', ['normal', 'gallery', 'direct_media']).default('normal') 16 | table.boolean('reddit').default(True) 17 | table.boolean('threads').default(True) 18 | table.boolean('bluesky').default(True) 19 | table.boolean('pixiv').default(True) 20 | table.boolean('ifunny').default(True) 21 | table.boolean('furaffinity').default(True) 22 | table.boolean('youtube').default(False) 23 | 24 | def down(self): 25 | """ 26 | Revert the migrations. 27 | """ 28 | with self.schema.table("guilds") as table: 29 | table.drop_column("instagram_view") 30 | table.drop_column("twitter_view") 31 | table.drop_column("tiktok") 32 | table.drop_column("tiktok_view") 33 | table.drop_column("reddit") 34 | table.drop_column("threads") 35 | table.drop_column("bluesky") 36 | table.drop_column("pixiv") 37 | table.drop_column("ifunny") 38 | table.drop_column("furaffinity") 39 | table.drop_column("youtube") 40 | -------------------------------------------------------------------------------- /database/migrations/2024_11_02_214523_remove_instagram_view.py: -------------------------------------------------------------------------------- 1 | """RemoveInstagramViews Migration.""" 2 | 3 | from masoniteorm.migrations import Migration 4 | 5 | 6 | class RemoveInstagramView(Migration): 7 | def up(self): 8 | """ 9 | Run the migrations. 10 | """ 11 | with self.schema.table("guilds") as table: 12 | table.drop_column("instagram_view") 13 | 14 | def down(self): 15 | """ 16 | Revert the migrations. 17 | """ 18 | with self.schema.table("guilds") as table: 19 | table.enum("instagram_view", ["normal", "gallery", "direct_media"]).default("normal") 20 | -------------------------------------------------------------------------------- /database/migrations/2024_11_02_220821_add_bluesky_view.py: -------------------------------------------------------------------------------- 1 | """AddBlueskyView Migration.""" 2 | 3 | from masoniteorm.migrations import Migration 4 | 5 | 6 | class AddBlueskyView(Migration): 7 | def up(self): 8 | """ 9 | Run the migrations. 10 | """ 11 | with self.schema.table("guilds") as table: 12 | table.enum('bluesky_view', ['normal', 'direct_media']).default('normal') 13 | 14 | def down(self): 15 | """ 16 | Revert the migrations. 17 | """ 18 | with self.schema.table("guilds") as table: 19 | table.drop_column('bluesky_view') 20 | -------------------------------------------------------------------------------- /database/migrations/2025_01_06_222619_new_bluesky_view.py: -------------------------------------------------------------------------------- 1 | """NewBlueskyView Migration.""" 2 | 3 | from masoniteorm.migrations import Migration 4 | 5 | 6 | class NewBlueskyView(Migration): 7 | def up(self): 8 | """ 9 | Run the migrations. 10 | """ 11 | with self.schema.table("guilds") as table: 12 | table.enum('bluesky_view', ['normal', 'direct_media', 'gallery']).default('normal').change() 13 | 14 | def down(self): 15 | """ 16 | Revert the migrations. 17 | """ 18 | with self.schema.table("guilds") as table: 19 | table.enum('bluesky_view', ['normal', 'direct_media']).default('normal').change() 20 | -------------------------------------------------------------------------------- /database/migrations/2025_01_11_221023_add_snapchat.py: -------------------------------------------------------------------------------- 1 | """AddSnapchat Migration.""" 2 | 3 | from masoniteorm.migrations import Migration 4 | 5 | 6 | class AddSnapchat(Migration): 7 | def up(self): 8 | """ 9 | Run the migrations. 10 | """ 11 | with self.schema.table("guilds") as table: 12 | table.boolean('snapchat').default(True) 13 | 14 | def down(self): 15 | """ 16 | Revert the migrations. 17 | """ 18 | with self.schema.table("guilds") as table: 19 | table.drop_column('snapchat') 20 | -------------------------------------------------------------------------------- /database/migrations/2025_01_26_140544_refacto_members.py: -------------------------------------------------------------------------------- 1 | """RefactoMembers Migration.""" 2 | 3 | from masoniteorm.migrations import Migration 4 | from masoniteorm.query import QueryBuilder 5 | 6 | 7 | class RefactoMembers(Migration): 8 | def up(self): 9 | """ 10 | Run the migrations. 11 | """ 12 | 13 | self.schema.rename("members", "old_members") 14 | 15 | with self.schema.table("old_members") as table: 16 | table.drop_foreign("members_guild_id_foreign") 17 | table.drop_index("members_guild_id_index") 18 | table.drop_unique("members_id_unique") 19 | 20 | with self.schema.create("members") as table: 21 | table.increments("id").primary().unique() 22 | 23 | table.big_integer("user_id").unsigned().index() 24 | 25 | table.big_integer("guild_id").unsigned().index() 26 | table.foreign("guild_id").references("id").on("guilds").on_delete("cascade") 27 | 28 | table.boolean("enabled").default(True) 29 | 30 | table.timestamps() 31 | 32 | old_members = QueryBuilder().on(self.connection).table('old_members') 33 | members = QueryBuilder().on(self.connection).table('members') 34 | 35 | members_to_create = [ 36 | { 37 | "user_id": member["id"], 38 | "guild_id": member["guild_id"], 39 | "enabled": member["enabled"], 40 | "created_at": member["created_at"], 41 | "updated_at": member["updated_at"], 42 | } 43 | for member in old_members.all() 44 | ] 45 | members_to_create_batches = [ 46 | members_to_create[i:i + 1000] 47 | for i in range(0, len(members_to_create), 1000) 48 | ] 49 | 50 | for batch in members_to_create_batches: 51 | members.bulk_create(batch) 52 | 53 | self.schema.drop("old_members") 54 | 55 | def down(self): 56 | """ 57 | Revert the migrations. 58 | """ 59 | 60 | self.schema.rename("members", "new_members") 61 | 62 | with self.schema.table("new_members") as table: 63 | table.drop_foreign("members_guild_id_foreign") 64 | table.drop_index("members_guild_id_index") 65 | table.drop_unique("members_id_unique") 66 | 67 | with self.schema.create("members") as table: 68 | table.big_integer("id").unsigned().unique() 69 | 70 | table.big_integer("guild_id").unsigned().index() 71 | table.foreign("guild_id").references("id").on("guilds").on_delete("cascade") 72 | 73 | table.boolean("enabled").default(True) 74 | 75 | table.timestamps() 76 | 77 | new_members = QueryBuilder().on(self.connection).table('new_members') 78 | members = QueryBuilder().on(self.connection).table('members') 79 | 80 | members_to_create = [ 81 | { 82 | "id": member["user_id"], 83 | "guild_id": member["guild_id"], 84 | "enabled": member["enabled"], 85 | "created_at": member["created_at"], 86 | "updated_at": member["updated_at"], 87 | } 88 | for member in new_members.all() 89 | ] 90 | members_to_create_batches = [ 91 | members_to_create[i:i + 1000] 92 | for i in range(0, len(members_to_create), 1000) 93 | ] 94 | 95 | for batch in members_to_create_batches: 96 | members.bulk_create(batch) 97 | 98 | self.schema.drop("new_members") 99 | -------------------------------------------------------------------------------- /database/migrations/2025_01_26_223431_add_more_websites.py: -------------------------------------------------------------------------------- 1 | """AddMoreWebsites Migration.""" 2 | 3 | from masoniteorm.migrations import Migration 4 | 5 | 6 | class AddMoreWebsites(Migration): 7 | def up(self): 8 | """ 9 | Run the migrations. 10 | """ 11 | with self.schema.table("guilds") as table: 12 | table.boolean('mastodon').default(False) 13 | table.boolean('deviantart').default(True) 14 | table.boolean('tumblr').default(False) 15 | table.boolean('facebook').default(True) 16 | table.boolean('bilibili').default(True) 17 | 18 | def down(self): 19 | """ 20 | Revert the migrations. 21 | """ 22 | with self.schema.table("guilds") as table: 23 | table.drop_column('mastodon') 24 | table.drop_column('deviantart') 25 | table.drop_column('tumblr') 26 | table.drop_column('facebook') 27 | table.drop_column('bilibili') 28 | -------------------------------------------------------------------------------- /database/migrations/2025_03_16_145300_add_twitch_spotify.py: -------------------------------------------------------------------------------- 1 | """AddMoreWebsites Migration.""" 2 | 3 | from masoniteorm.migrations import Migration 4 | 5 | 6 | class AddTwitchSpotify(Migration): 7 | def up(self): 8 | """ 9 | Run the migrations. 10 | """ 11 | with self.schema.table("guilds") as table: 12 | table.boolean('twitch').default(True) 13 | table.boolean('spotify').default(False) 14 | 15 | def down(self): 16 | """ 17 | Revert the migrations. 18 | """ 19 | with self.schema.table("guilds") as table: 20 | table.drop_column('twitch') 21 | table.drop_column('spotify') 22 | -------------------------------------------------------------------------------- /database/migrations/2025_03_16_211708_create_events.py: -------------------------------------------------------------------------------- 1 | """CreateEvents Migration.""" 2 | 3 | from masoniteorm.migrations import Migration 4 | 5 | 6 | class CreateEvents(Migration): 7 | def up(self): 8 | """ 9 | Run the migrations. 10 | """ 11 | with self.schema.create("events") as table: 12 | table.text("name") 13 | table.timestamps() 14 | 15 | def down(self): 16 | """ 17 | Revert the migrations. 18 | """ 19 | self.schema.drop("events") 20 | -------------------------------------------------------------------------------- /database/migrations/2025_03_16_234024_add_bot_members.py: -------------------------------------------------------------------------------- 1 | """AddBotMembers Migration.""" 2 | 3 | from masoniteorm.migrations import Migration 4 | 5 | 6 | class AddBotMembers(Migration): 7 | def up(self): 8 | """ 9 | Run the migrations. 10 | """ 11 | with self.schema.table("members") as table: 12 | table.boolean("bot").default(False) 13 | 14 | def down(self): 15 | """ 16 | Revert the migrations. 17 | """ 18 | with self.schema.table("members") as table: 19 | table.drop_column("bot") 20 | -------------------------------------------------------------------------------- /database/migrations/2025_03_18_133740_add_webhook_support.py: -------------------------------------------------------------------------------- 1 | """AddWebhookSupport Migration.""" 2 | 3 | from masoniteorm.migrations import Migration 4 | 5 | 6 | class AddWebhookSupport(Migration): 7 | def up(self): 8 | """ 9 | Run the migrations. 10 | """ 11 | with self.schema.table("guilds") as table: 12 | table.boolean("webhooks").default(False) 13 | 14 | def down(self): 15 | """ 16 | Revert the migrations. 17 | """ 18 | with self.schema.table("guilds") as table: 19 | table.drop_column("webhooks") 20 | -------------------------------------------------------------------------------- /database/migrations/2025_06_03_021604_add_silent_replies.py: -------------------------------------------------------------------------------- 1 | """AddSilentReplies Migration.""" 2 | 3 | from masoniteorm.migrations import Migration 4 | 5 | 6 | class AddSilentReplies(Migration): 7 | def up(self): 8 | """ 9 | Run the migrations. 10 | """ 11 | with self.schema.table("guilds") as table: 12 | table.rename("reply", "reply_to_message", "boolean") 13 | table.boolean("reply_to_message").default(False).change() 14 | table.boolean("reply_silently").after("reply").default(True) 15 | 16 | def down(self): 17 | """ 18 | Revert the migrations. 19 | """ 20 | with self.schema.table("guilds") as table: 21 | table.rename("reply_to_message", "reply", "boolean") 22 | table.boolean("reply").default(False).change() 23 | table.drop_column("reply_silently") 24 | -------------------------------------------------------------------------------- /database/models/CustomWebsite.py: -------------------------------------------------------------------------------- 1 | """ CustomWebsite Model """ 2 | 3 | from typing import Optional 4 | 5 | from masoniteorm.models import Model 6 | from masoniteorm.relationships import belongs_to 7 | 8 | 9 | class CustomWebsite(Model): 10 | """CustomWebsite Model""" 11 | 12 | __table__ = "custom_websites" 13 | 14 | @belongs_to 15 | def guild(self): 16 | from database.models.Guild import Guild 17 | return Guild 18 | 19 | @classmethod 20 | def find_or_create(cls, guild_id, website_id: int, guild_kwargs: Optional[dict] = None, **kwargs): 21 | website = cls.find(website_id) 22 | if website is None: 23 | from database.models.Guild import Guild 24 | if isinstance(guild_id, Guild): 25 | guild = guild_id 26 | else: 27 | guild = Guild.find_or_create(guild_id, **(guild_kwargs or {})) 28 | website = cls.create({'id': website_id, 'guild_id': guild.id, **kwargs}).fresh() 29 | return website 30 | -------------------------------------------------------------------------------- /database/models/Event.py: -------------------------------------------------------------------------------- 1 | """ Event Model """ 2 | 3 | from masoniteorm.models import Model 4 | from typing import Self 5 | import datetime as dt 6 | 7 | 8 | class Event(Model): 9 | """Event Model""" 10 | 11 | __table__ = "events" 12 | 13 | @classmethod 14 | def since(cls, days: int = 1, hours: int = 0, minutes: int = 0, seconds: int = 0) -> list[Self]: 15 | """ 16 | Get all events since a certain time 17 | 18 | :param days: the number of days 19 | :param hours: the number of hours 20 | :param minutes: the number of minutes 21 | :param seconds: the number of seconds 22 | 23 | :return: the events since the time 24 | """ 25 | 26 | return cls.where('created_at', '>=', dt.datetime.now() - dt.timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)).get() 27 | -------------------------------------------------------------------------------- /database/models/Guild.py: -------------------------------------------------------------------------------- 1 | """ Guild Model """ 2 | from enum import Enum 3 | from typing import Self 4 | 5 | from masoniteorm.models import Model 6 | from masoniteorm.relationships import has_many 7 | 8 | 9 | __all__ = ('Guild', 'OriginalMessage', 'TwitterView', 'TiktokView', 'BlueskyView') 10 | 11 | 12 | class OriginalMessage(Enum): 13 | NOTHING = 'nothing' 14 | REMOVE_EMBEDS = 'remove_embeds' 15 | DELETE = 'delete' 16 | 17 | def get(self, value: str) -> Self: 18 | return self.__members__.get(value) 19 | 20 | def set(self, value: Self) -> str: 21 | return value.name 22 | 23 | 24 | class TwitterView(Enum): 25 | NORMAL = 'normal' 26 | GALLERY = 'gallery' 27 | TEXT_ONLY = 'text_only' 28 | DIRECT_MEDIA = 'direct_media' 29 | 30 | def get(self, value: str) -> Self: 31 | return self.__members__.get(value) 32 | 33 | def set(self, value: Self) -> str: 34 | return value.name 35 | 36 | 37 | class TiktokView(Enum): 38 | NORMAL = 'normal' 39 | GALLERY = 'gallery' 40 | DIRECT_MEDIA = 'direct_media' 41 | 42 | def get(self, value: str) -> Self: 43 | return self.__members__.get(value) 44 | 45 | def set(self, value: Self) -> str: 46 | return value.name 47 | 48 | class BlueskyView(Enum): 49 | NORMAL = 'normal' 50 | DIRECT_MEDIA = 'direct_media' 51 | GALLERY = 'gallery' 52 | 53 | def get(self, value: str) -> Self: 54 | return self.__members__.get(value) 55 | 56 | def set(self, value: Self) -> str: 57 | return value.name 58 | 59 | 60 | class Guild(Model): 61 | """Guild Model""" 62 | 63 | __table__ = "guilds" 64 | 65 | __casts__ = { 66 | 'reply_to_message': 'bool', 67 | 'reply_silently': 'bool', 68 | 'webhooks': 'bool', 69 | 'original_message': OriginalMessage, 70 | 'twitter_view': TwitterView, 71 | 'tiktok_view': TiktokView, 72 | 'bluesky_view': BlueskyView, 73 | 'default_channel_state': 'bool', 74 | 'default_member_state': 'bool', 75 | 'default_role_state': 'bool', 76 | } 77 | 78 | @has_many('id', 'guild_id') 79 | def text_channels(self): 80 | from database.models.TextChannel import TextChannel 81 | return TextChannel 82 | 83 | @has_many('id', 'guild_id') 84 | def members(self): 85 | from database.models.Member import Member 86 | return Member 87 | 88 | @has_many('id', 'guild_id') 89 | def custom_websites(self): 90 | from database.models.CustomWebsite import CustomWebsite 91 | return CustomWebsite 92 | 93 | @classmethod 94 | def find_or_create(cls, guild_id: int, **kwargs): 95 | guild = cls.find(guild_id) 96 | if guild is None: 97 | guild = cls.create({'id': guild_id, **kwargs}).fresh() 98 | return guild 99 | -------------------------------------------------------------------------------- /database/models/Member.py: -------------------------------------------------------------------------------- 1 | """ Member Model """ 2 | from __future__ import annotations 3 | 4 | from typing import Optional, TYPE_CHECKING 5 | 6 | import discore 7 | from masoniteorm.models import Model 8 | from masoniteorm.relationships import belongs_to 9 | 10 | if TYPE_CHECKING: 11 | from database.models.Guild import Guild 12 | 13 | 14 | class Member(Model): 15 | """Member Model""" 16 | 17 | __table__ = "members" 18 | 19 | __casts__ = {'enabled': 'bool'} 20 | 21 | @belongs_to 22 | def guild(self): 23 | from database.models.Guild import Guild 24 | return Guild 25 | 26 | @classmethod 27 | def find_or_create(cls, d_member: discore.Member, guild: Optional[Guild] = None, guild_kwargs: Optional[dict] = None, **kwargs): 28 | member = cls.where('user_id', d_member.id).where('guild_id', guild.id).first() 29 | if member: 30 | return member 31 | 32 | if guild is None: 33 | from database.models.Guild import Guild 34 | guild = Guild.find_or_create(d_member.guild.id, **(guild_kwargs or {})) 35 | return cls.create({ 36 | 'user_id': d_member.id, 37 | 'guild_id': d_member.guild.id, 38 | 'enabled': guild.default_member_state if not d_member.bot else False, 39 | 'bot': d_member.bot, 40 | **kwargs 41 | }).fresh() 42 | 43 | @classmethod 44 | def update_guild_members(cls, guild: discore.Guild, ignored_members: list[int], default_state: bool = True) -> None: 45 | """ 46 | Update the members of a guild 47 | :param guild: The guild to update the members for 48 | :param ignored_members: A list of member IDs to ignore 49 | :param default_state: The default state for the created members 50 | :return: None 51 | """ 52 | 53 | all_members = [member for member in guild.members if member.id not in ignored_members] 54 | all_db_members = cls.where('guild_id', guild.id).where_not_in('user_id', ignored_members).get() 55 | missing_from_db = [ 56 | member for member in all_members if member.id not in [db_member.user_id for db_member in all_db_members]] 57 | missing_from_discord = [member.user_id for member in all_db_members if member.user_id not in [m.id for m in all_members]] 58 | if missing_from_db: 59 | # noinspection PyUnresolvedReferences 60 | cls.builder.new().bulk_create([ 61 | { 62 | 'user_id': member.id, 63 | 'guild_id': guild.id, 64 | 'enabled': default_state if not member.bot else False, 65 | 'bot': member.bot 66 | } for member in missing_from_db 67 | ]) 68 | if missing_from_discord: 69 | cls.where('guild_id', guild.id).where_in('user_id', missing_from_discord).delete() 70 | -------------------------------------------------------------------------------- /database/models/Role.py: -------------------------------------------------------------------------------- 1 | """ Role Model """ 2 | 3 | from typing import Optional, List, Self 4 | 5 | import discore 6 | from masoniteorm.models import Model 7 | from masoniteorm.relationships import belongs_to 8 | 9 | 10 | class Role(Model): 11 | """Role Model""" 12 | 13 | __table__ = "roles" 14 | 15 | __casts__ = {'enabled': 'bool'} 16 | 17 | @belongs_to 18 | def guild(self): 19 | from database.models.Guild import Guild 20 | return Guild 21 | 22 | @classmethod 23 | def find_or_create(cls, guild, role_id: int, guild_kwargs: Optional[dict] = None, **kwargs): 24 | role = cls.find(role_id) 25 | if role is None: 26 | from database.models.Guild import Guild 27 | if isinstance(guild, int): 28 | guild = Guild.find_or_create(guild, **(guild_kwargs or {})) 29 | role = cls.create( 30 | {'id': role_id, 'guild_id': guild.id, 'enabled': guild.default_role_state, **kwargs}).fresh() 31 | return role 32 | 33 | @classmethod 34 | def finds_or_creates(cls, guild, roles_id: list[int], guild_kwargs: Optional[dict] = None, **kwargs) -> List[Self]: 35 | """ 36 | Check if all roles are enabled 37 | :param guild: The guild to check 38 | :param roles_id: The roles to check 39 | :param guild_kwargs: The guild kwargs 40 | :param kwargs: The kwargs 41 | :return: True if all roles are enabled 42 | """ 43 | # noinspection PyTypeChecker 44 | db_roles: List[Role] = cls.find(roles_id) 45 | # noinspection PyUnresolvedReferences 46 | missing_roles = [role for role in roles_id if role not in [db_role.id for db_role in db_roles]] 47 | if missing_roles: 48 | from database.models.Guild import Guild 49 | if isinstance(guild, int): 50 | guild = Guild.find_or_create(guild, **(guild_kwargs or {})) 51 | # noinspection PyUnresolvedReferences 52 | cls.builder.new().bulk_create([ 53 | {'id': role, 'guild_id': guild.id, 'enabled': guild.default_role_state, **kwargs} 54 | for role in missing_roles 55 | ]) 56 | db_roles = cls.where_in('id', roles_id).where('guild_id', guild.id).get() 57 | # noinspection PyUnresolvedReferences 58 | return db_roles 59 | 60 | @classmethod 61 | def update_guild_roles(cls, guild: discore.Guild, ignored_roles: list[int], default_state: bool = True) -> None: 62 | """ 63 | Update the roles of a guild in the database 64 | :param guild: The guild to update 65 | :param ignored_roles: The roles to ignore 66 | :param default_state: The default state for the created roles 67 | """ 68 | 69 | all_roles = [role.id for role in guild.roles if role.id not in ignored_roles] 70 | all_db_roles = cls.where('guild_id', guild.id).where_not_in('id', ignored_roles).get() 71 | missing_from_db = [i for i in all_roles if i not in [c.id for c in all_db_roles]] 72 | missing_from_discord = [i.id for i in all_db_roles if i.id not in all_roles] 73 | if missing_from_db: 74 | # noinspection PyUnresolvedReferences 75 | cls.builder.new().bulk_create([ 76 | {'id': i, 'guild_id': guild.id, 'enabled': default_state} for i in missing_from_db 77 | ]) 78 | if missing_from_discord: 79 | cls.where('guild_id', guild.id).where_in('id', missing_from_discord).delete() 80 | -------------------------------------------------------------------------------- /database/models/TextChannel.py: -------------------------------------------------------------------------------- 1 | """ TextChannel Model """ 2 | from typing import Optional 3 | 4 | import discore 5 | from masoniteorm.models import Model 6 | from masoniteorm.relationships import belongs_to 7 | 8 | 9 | class TextChannel(Model): 10 | """TextChannel Model""" 11 | 12 | __table__ = "text_channels" 13 | 14 | __casts__ = {'enabled': 'bool'} 15 | 16 | @belongs_to 17 | def guild(self): 18 | from database.models.Guild import Guild 19 | return Guild 20 | 21 | @classmethod 22 | def find_or_create(cls, guild, channel_id: int, guild_kwargs: Optional[dict] = None, **kwargs): 23 | channel = cls.find(channel_id) 24 | if channel is None: 25 | from database.models.Guild import Guild 26 | if isinstance(guild, int): 27 | guild = Guild.find_or_create(guild, **(guild_kwargs or {})) 28 | channel = cls.create( 29 | {'id': channel_id, 'guild_id': guild.id, 'enabled': guild.default_channel_state, **kwargs}).fresh() 30 | return channel 31 | 32 | @classmethod 33 | def update_guild_channels(cls, guild: discore.Guild, ignored_channels: list[int], default_state: bool = True) -> None: 34 | """ 35 | Update the channels of a guild in the database 36 | :param guild: The guild to update 37 | :param ignored_channels: The channels to ignore 38 | :param default_state: The default state for the created channels 39 | """ 40 | 41 | all_channels = [channel.id for channel in guild.text_channels + [*guild.threads] 42 | if channel.id not in ignored_channels] 43 | all_db_channels = cls.where('guild_id', guild.id).where_not_in('id', ignored_channels).get() 44 | missing_from_db = [i for i in all_channels if i not in [c.id for c in all_db_channels]] 45 | missing_from_discord = [i.id for i in all_db_channels if i.id not in all_channels] 46 | if missing_from_db: 47 | # noinspection PyUnresolvedReferences 48 | cls.builder.new().bulk_create([ 49 | {'id': i, 'guild_id': guild.id, 'enabled': default_state} for i in missing_from_db 50 | ]) 51 | if missing_from_discord: 52 | cls.where('guild_id', guild.id).where_in('id', missing_from_discord).delete() 53 | -------------------------------------------------------------------------------- /docker-compose.example.yml: -------------------------------------------------------------------------------- 1 | services: 2 | bot: 3 | image: fixtweetbot:latest 4 | restart: unless-stopped 5 | depends_on: 6 | - db 7 | # You do NOT need to repeat these configuration values in the override file. They are automatically supplied to the bot. 8 | environment: 9 | # Should be equal to the name of the database container. 10 | - DATABASE_HOST=db 11 | - DATABASE_PORT=3306 12 | # Should be equal to MYSQL_DATABASE. 13 | - DATABASE_NAME=fixtweetbot 14 | # Should be equal to MYSQL_USER. 15 | - DATABASE_USER=fixtweetbot 16 | # Should be equal to MYSQL_PASSWORD. 17 | - DATABASE_PASSWORD=changeme123 18 | # Your Discord bot token. 19 | - DISCORD_TOKEN=your_discord_bot_token 20 | # Optional: Server ID that you want to use for controlling your bot's instance. 21 | # - DEV_GUILD=your_discord_dev_guild_id 22 | # uncomment and create file if you want to override any default settings 23 | # volumes: 24 | # - ./override.config.yml:/usr/local/app/override.config.yml:ro 25 | 26 | db: 27 | image: mysql:8.0 28 | restart: unless-stopped 29 | environment: 30 | - MYSQL_DATABASE=fixtweetbot 31 | - MYSQL_USER=fixtweetbot 32 | # Generate a random password for this field. 33 | - MYSQL_PASSWORD=changeme123 34 | # Generate a random password for this field. Do not use the same password! 35 | - MYSQL_ROOT_PASSWORD=changeme234 36 | - TZ=UTC 37 | volumes: 38 | - mysql_data:/var/lib/mysql 39 | healthcheck: 40 | test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$MYSQL_ROOT_PASSWORD"] 41 | interval: 10s 42 | timeout: 5s 43 | retries: 5 44 | start_period: 30s 45 | ports: 46 | - "3306:3306" 47 | 48 | volumes: 49 | mysql_data: 50 | driver: local 51 | -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | __config=" 5 | database: 6 | host: $DATABASE_HOST 7 | port: $DATABASE_PORT 8 | user: $DATABASE_USER 9 | driver: $DATABASE_DRIVER 10 | password: $DATABASE_PASSWORD 11 | database: $DATABASE_NAME 12 | 13 | token: $DISCORD_TOKEN" 14 | 15 | if [ -n "$DEV_GUILD" ]; then 16 | __config=" 17 | $__config 18 | dev_guild: $DEV_GUILD" 19 | fi 20 | 21 | echo "$__config" > /usr/local/app/docker.config.yml 22 | 23 | echo -n "Waiting for database.." 24 | while ! nc -z $DATABASE_HOST $DATABASE_PORT 2>/dev/null; do 25 | echo -n "." 26 | sleep 1 27 | done 28 | 29 | 30 | echo -e \\n"Database ready" 31 | 32 | masonite-orm migrate -C database/config.py -d database/migrations || echo "Migration failed but continuing..." 33 | 34 | exec "$@" 35 | -------------------------------------------------------------------------------- /locales/bg.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | -------------------------------------------------------------------------------- /locales/cs.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | -------------------------------------------------------------------------------- /locales/da.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | -------------------------------------------------------------------------------- /locales/de.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | -------------------------------------------------------------------------------- /locales/el.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | -------------------------------------------------------------------------------- /locales/en-GB.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | about: 3 | command: 4 | name: "about" 5 | description: "Obtain information about the bot" 6 | name: "About" 7 | description: "This bot automatically reposts social media links in a 'fixed' format." 8 | help: 9 | name: "Help" 10 | value: "Use `/settings` to configure the bot on your server. If you encounter any issues with the bot, use the Troubleshooting section of the `/settings` command. Visit the [support server](https://discord.gg/3ej9JrkF3U) for further assistance." 11 | premium: 12 | name: "Premium" 13 | "true": "✨ This server has premium features activated! ✨" 14 | "false": "This server is not a premium server." 15 | invite: "Invitation" 16 | source: "Source code" 17 | support: "Support server" 18 | links: 19 | name: "Links" 20 | value: | 21 | - [Invite link](%{invite_link}) 22 | - [Tog.gg Page](https://top.gg/bot/1164651057243238400) (please vote!) 23 | - [Source code](%{repo_link}) (please leave a star!) 24 | - [Translation project](https://crowdin.com/project/fixtweetbot) (help us translate in your language!) 25 | - [Proxies/Fixers credits](https://github.com/Kyrela/FixTweetBot?tab=readme-ov-file#proxies) 26 | - [Support server](%{support_link}) 27 | settings: 28 | command: 29 | name: "settings" 30 | description: "Manage the FixTweet configuration" 31 | channel: 32 | name: "channel" 33 | description: "The channel in which to manage FixTweet settings" 34 | member: 35 | name: "member" 36 | description: "The member for whom FixTweet settings are managed" 37 | role: 38 | name: "role" 39 | description: "The role for which FixTweet settings are managed" 40 | title: "Settings" 41 | description: "Choose a setting to view its details" 42 | placeholder: "Choose a setting" 43 | perms: 44 | scope: " in %{scope}" 45 | label: "\n\nPermissions within %{channel}:\n" 46 | -------------------------------------------------------------------------------- /locales/en-US.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | about: 3 | command: 4 | name: "about" 5 | description: "Get information about the bot" 6 | name: "About" 7 | description: "This bot automatically repost social media links as a 'fixed' version." 8 | help: 9 | name: "Help" 10 | value: "Use `/settings` to configure the bot in your server. 11 | If you encounter issues with the bot, use the `Troubleshooting` section of the `/settings` command. 12 | Visit the [support server](https://discord.gg/3ej9JrkF3U) for more help." 13 | premium: 14 | name: "Premium" 15 | "true": "✨ This server has premium features enabled! ✨" 16 | "false": "This server isn't premium." 17 | invite: "Invite" 18 | source: "Source code" 19 | support: "Support server" 20 | links: 21 | name: "Links" 22 | value: | 23 | - [Invite link](%{invite_link}) 24 | - [Tog.gg Page](https://top.gg/bot/1164651057243238400) (please vote!) 25 | - [Source code](%{repo_link}) (please leave a star!) 26 | - [Translation project](https://crowdin.com/project/fixtweetbot) (help us translate in your language!) 27 | - [Proxies/Fixers credits](https://github.com/Kyrela/FixTweetBot?tab=readme-ov-file#proxies) 28 | - [Support server](%{support_link}) 29 | 30 | settings: 31 | command: 32 | name: "settings" 33 | description: "Manage the FixTweet settings" 34 | channel: 35 | name: "channel" 36 | description: "The channel in which to manage FixTweet settings" 37 | member: 38 | name: "member" 39 | description: "The member for whom to manage FixTweet settings" 40 | role: 41 | name: "role" 42 | description: "The role for which to manage FixTweet settings" 43 | title: "Settings" 44 | description: "Select a setting to view its details" 45 | placeholder: "Select a setting" 46 | perms: 47 | scope: " in %{scope}" 48 | label: "\n\nPermissions in %{channel}:\n" 49 | missing_label: "\n\n**Missing permissions:**\n" 50 | view_channel: 51 | "true": "🟢 `View channel` permission" 52 | "false": "🔴 Missing `view channel` permission" 53 | send_messages: 54 | "true": "🟢 `Send message` permission" 55 | "false": "🔴 Missing `send message` permission" 56 | send_messages_in_threads: 57 | "true": "🟢 `Send message in threads` permission" 58 | "false": "🔴 Missing `send message in threads` permission" 59 | embed_links: 60 | "true": "🟢 `Embed links` permission" 61 | "false": "🔴 Missing `embed links` permission" 62 | manage_messages: 63 | "true": "🟢 `Manage messages` permission" 64 | "false": "🔴 Missing `manage messages` permission" 65 | read_message_history: 66 | "true": "🟢 `Read message history` permission" 67 | "false": "🔴 Missing `read message history` permission" 68 | channel: 69 | name: "Channel" 70 | description: "Enable/Disable on a channel" 71 | content: "**Enable/Disable %{bot} in %{channel}**\n- %{state}\n- %{default_state}%{perms}" 72 | toggle: 73 | "true": "Enabled" 74 | "false": "Disabled" 75 | toggle_all: 76 | "true": "Enabled everywhere" 77 | "false": "Disabled everywhere" 78 | "none": "Enable/Disable everywhere" 79 | toggle_default: 80 | "true": "Enabled for new channels" 81 | "false": "Disabled for new channels" 82 | "premium": "Change for new channels (premium feature)" 83 | state: 84 | "true": "🟢 Enabled in %{channel}" 85 | "false": "🔴 Disabled in %{channel}" 86 | all_state: 87 | "true": "🟢 Enabled in all channels" 88 | "false": "🔴 Disabled in all channels" 89 | default_state: 90 | "true": "🟢 Enabled for new channels (by default)" 91 | "false": "🔴 Disabled for new channels (by default)" 92 | member: 93 | name: "Member" 94 | description: "Enable/Disable on a member" 95 | content: "**Enable/Disable %{bot} for %{member}**\n- %{state}\n- %{default_state}" 96 | toggle: 97 | "true": "Enabled" 98 | "false": "Disabled" 99 | toggle_all: 100 | "true": "Enabled for everyone" 101 | "false": "Disabled for everyone" 102 | "none": "Enable/Disable for everyone" 103 | toggle_default: 104 | "true": "Enabled for new members" 105 | "false": "Disabled for new members" 106 | premium: "Change for new members (premium feature)" 107 | state: 108 | "true": "🟢 Enabled for %{member}" 109 | "false": "🔴 Disabled for %{member}" 110 | all_state: 111 | "true": "🟢 Enabled for everyone" 112 | "false": "🔴 Disabled for everyone" 113 | default_state: 114 | "true": "🟢 Enabled for new members (by default)" 115 | "false": "🔴 Disabled for new members (by default)" 116 | role: 117 | name: "Role" 118 | description: "Enable/Disable on a role" 119 | content: "**Enable/Disable %{bot} for %{role}**\n- %{state}\n- %{default_state}" 120 | toggle: 121 | "true": "Enabled" 122 | "false": "Disabled" 123 | toggle_all: 124 | "true": "Enabled for every role" 125 | "false": "Disabled for every role" 126 | "none": "Enable/Disable for every role" 127 | toggle_default: 128 | "true": "Enabled for new roles" 129 | "false": "Disabled for new roles" 130 | premium: "Change for new roles (premium feature)" 131 | state: 132 | "true": "🟢 Enabled for %{role}" 133 | "false": "🔴 Disabled for %{role}" 134 | all_state: 135 | "true": "🟢 Enabled for every role" 136 | "false": "🔴 Disabled for every role" 137 | default_state: 138 | "true": "🟢 Enabled for new roles (by default)" 139 | "false": "🔴 Disabled for new roles (by default)" 140 | reply_method: 141 | name: "Reply method" 142 | description: "Change the behavior on the reply" 143 | content: "**Change what to do on the reply**\n- %{state}\n- %{silent}%{perms}" 144 | reply: 145 | button: 146 | "true": "Replying" 147 | "false": "Sending" 148 | state: 149 | "true": "%{emoji} Replying to messages" 150 | "false": "📨 Just sending the message" 151 | silent: 152 | button: 153 | "true": "Silent" 154 | "false": "With notification" 155 | state: 156 | "true": "🔕 Messages will be sent silently" 157 | "false": "🔔 Messages will be sent with a notification" 158 | webhooks: 159 | name: "Webhooks" 160 | description: "Enable/Disable for webhooks" 161 | content: "**Change the behavior on webhooks**\n%{state}" 162 | button: 163 | "true": "Replying" 164 | "false": "Ignoring" 165 | state: 166 | "true": "🟢 Replying to webhooks" 167 | "false": "🔴 Ignoring webhooks" 168 | original_message: 169 | name: "Original message" 170 | description: "Change the behavior on the original message" 171 | content: "**Change what to do on the original message**\n%{state}%{perms}" 172 | option: 173 | nothing: 174 | label: "Do nothing" 175 | emoji: "🚫" 176 | remove_embeds: 177 | label: "Remove the embeds" 178 | emoji: "✂️" 179 | delete: 180 | label: "Completely delete the message" 181 | emoji: "🗑️" 182 | troubleshooting: 183 | name: "Troubleshooting" 184 | description: "Check the bot's status and troubleshoot common issues" 185 | ping: 186 | name: "Ping" 187 | value: "%{latency} ms" 188 | premium: 189 | name: "Premium" 190 | "true": "✨ This server has premium features enabled! ✨" 191 | "false": "This server isn't premium." 192 | permissions: "Permissions in %{channel}" 193 | options: "Options" 194 | websites: "Websites" 195 | refresh: "Refresh" 196 | custom_websites: "Custom websites" 197 | websites: 198 | name: "Websites" 199 | description: "Change the different websites settings" 200 | placeholder: "Select a website to edit" 201 | content: "**Change the different websites settings**\n\nSelect a website to edit its settings" 202 | base_website: 203 | description: "Change the %{name} links settings" 204 | content: "**Enable/Disable the %{name} link fix**\n%{state}%{view}\n-# Credits: %{credits}" 205 | view: 206 | normal: 207 | label: "Normal view" 208 | emoji: "🔗" 209 | gallery: 210 | label: "Gallery view" 211 | emoji: "🖼️" 212 | text_only: 213 | label: "Text only view" 214 | emoji: "📝" 215 | direct_media: 216 | label: "Direct media view" 217 | emoji: "📸" 218 | state: 219 | "true": "🟢 Fixing %{name} links" 220 | "false": "🔴 Not fixing %{name} links" 221 | button: 222 | "true": "Enabled" 223 | "false": "Disabled" 224 | twitter: 225 | name: "Twitter" 226 | description: "Change the Twitter links settings" 227 | content: "**Enable/Disable the Twitter link fix and manage translations**\n%{state}\n%{view}\n-# Credits: %{credits}" 228 | state: 229 | "true": "🟢 Fixing Twitter links" 230 | "false": "🔴 Not fixing Twitter links" 231 | translation: 232 | "true": " and translating them to '%{lang}'" 233 | "false": " but not translating" 234 | button: 235 | state: 236 | "true": "Enabled" 237 | "false": "Disabled" 238 | translation: 239 | "true": "Translating to '%{lang}'" 240 | "false": "Translations disabled" 241 | translation_lang: "Edit translation language" 242 | modal: 243 | title: "Edit translation language" 244 | label: "Translation language" 245 | placeholder: "Enter the translation 2-letter ISO language code (e.g. 'en')" 246 | error: "'%{lang}' is not a valid language. Please enter a valid 2-letter ISO language code (e.g. 'en'). [List of ISO language codes]()" 247 | custom_websites: 248 | name: "Custom websites" 249 | description: "Add or remove custom websites to fix" 250 | content: "**Add or remove custom websites to fix**" 251 | list: "\n\nWebsites registered:\n" 252 | website: "- %{name}: `%{domain}` → `%{fix_domain}`" 253 | selected_website: "- **%{name}: `%{domain}` → `%{fix_domain}`**" 254 | placeholder: "Select the website to edit" 255 | empty: "No websites registered" 256 | button: 257 | add: "Add website" 258 | edit: "Edit website" 259 | delete: "Remove website" 260 | premium: "Add website (premium above 3 websites)" 261 | max: "Add website (can't add more than 25 websites)" 262 | modal: 263 | title: "Add a custom website" 264 | name: 265 | label: "Website name" 266 | placeholder: "Enter the website name (e.g. 'My website')" 267 | domain: 268 | label: "Website domain" 269 | placeholder: "Enter the website domain (e.g. 'mywebsite.com')" 270 | fix_domain: 271 | label: "Fixed domain" 272 | placeholder: "Enter the fixed domain (e.g. 'fxmywebsite.com')" 273 | error: 274 | exists: "This website already has a fix" 275 | length_name: "The length of the website name must be less than %{max} characters" 276 | length_domain: "The length of the website domain must be less than %{max} characters" 277 | 278 | misc: 279 | discord_discovery_description: | 280 | FixTweetBot is a Discord bot that fixes social media embeds, 281 | using online services (such as [FxTwitter](https://github.com/FixTweet/FxTwitter)) 282 | 283 | **In concrete terms, this bot automatically repost social media links as a 'fixed' version 284 | that contains a better embed (that allows to play videos directly in Discord, for example).** 285 | 286 | ## Features & Highlights 287 | 288 | - Supports Twitter, Nitter, Instagram, TikTok, Reddit, Threads, Bluesky, Snapchat, Facebook, Pixiv, Twitch, Spotify, 289 | DeviantArt, Mastodon, Tumblr, BiliBili, IFunny, Fur Affinity, YouTube, and any custom websites of your choice 290 | - Tweets translation 291 | - Disable by website, channel, member or role 292 | - Highly customizable behavior and appearance 293 | - Multiple languages supported 294 | - Modern interface for settings 295 | - Can respond to bots and webhooks 296 | - Respect markdown 297 | - Respect privacy 298 | - Source-available 299 | 300 | ## Usage 301 | 302 | Simply send a message containing a compatible social media link, and the bot will remove the embed if any and 303 | automatically repost it as a 'fixed' link. 304 | 305 | You also can ignore a link by putting it between `<` and `>`, like this: ``. 306 | 307 | You can manage the bot's settings with the `/settings` command. 308 | 309 | Lastly, you can use the `/about` command any time to get more information about the bot and to get help. 310 | -------------------------------------------------------------------------------- /locales/es-419.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | -------------------------------------------------------------------------------- /locales/es-ES.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | -------------------------------------------------------------------------------- /locales/fi.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | -------------------------------------------------------------------------------- /locales/fr.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | about: 3 | command: 4 | name: "à-propos" 5 | description: "Afficher des informations sur le bot" 6 | name: "À propos" 7 | description: "Ce bot reposte automatiquement les liens des réseaux sociaux en une version 'corrigée'." 8 | help: 9 | name: "Aide" 10 | value: "Utilisez `/paramètres` pour configurer le bot dans votre serveur. Si vous rencontrez des problèmes avec le bot, utilisez la section `Dépannage` de la commande `/paramètres`. Visitez le [serveur de support](https://discord.gg/3ej9JrkF3U) pour plus d'aide." 11 | premium: 12 | name: "Premium" 13 | "true": "✨ Ce serveur a les fonctionnalités premium activées ! ✨" 14 | "false": "Ce serveur n'est pas premium." 15 | invite: "Inviter" 16 | source: "Code source" 17 | support: "Serveur de support" 18 | links: 19 | name: "Liens" 20 | value: | 21 | - [Lien d'invitation](%{invite_link}) 22 | - [Page Tog.gg](https://top.gg/bot/1164651057243238400) (merci de voter !) 23 | - [Code source](%{repo_link}) (merci de laisser une étoile !) 24 | - [Projet de traduction](https://crowdin.com/project/fixtweetbot) (aidez-nous à traduire dans votre langue !) 25 | - [Crédits des proxies/fixers](https://github.com/Kyrela/FixTweetBot?tab=readme-ov-file#proxies) 26 | - [Serveur de support](%{support_link}) 27 | settings: 28 | command: 29 | name: "paramètres" 30 | description: "Gérer les paramètres de FixTweet" 31 | channel: 32 | name: "salon" 33 | description: "Le salon dans lequel gérer les paramètres de FixTweet" 34 | member: 35 | name: "membre" 36 | description: "Le membre pour lequel gérer les paramètres de FixTweet" 37 | role: 38 | name: "rôle" 39 | description: "Le rôle pour lequel gérer les paramètres de FixTweet" 40 | title: "Paramètres" 41 | description: "Sélectionnez un paramètre pour voir ses détails" 42 | placeholder: "Sélectionnez un paramètre" 43 | perms: 44 | scope: " dans %{scope}" 45 | label: "\n\nPermissions dans %{channel} :\n" 46 | missing_label: "\n\n**Permissions manquantes :**\n" 47 | view_channel: 48 | "true": "🟢 Permission `voir le salon`" 49 | "false": "🔴 Permission `voir le salon` manquante" 50 | send_messages: 51 | "true": "🟢 Permission `envoyer des messages`" 52 | "false": "🔴 Permission `envoyer des messages` manquante" 53 | send_messages_in_threads: 54 | "true": "🟢 Permission `envoyer des messages dans les fils`" 55 | "false": "🔴 Permission `envoyer des messages dans les fils` manquante" 56 | embed_links: 57 | "true": "🟢 Permission `intégrer des liens`" 58 | "false": "🔴 Permission `intégrer des liens` manquante" 59 | manage_messages: 60 | "true": "🟢 Permission `gérer les messages`" 61 | "false": "🔴 Permission `gérer les messages` manquante" 62 | read_message_history: 63 | "true": "🟢 Permission `voir les anciens messages`" 64 | "false": "🔴 Permission `voir les anciens messages` manquante" 65 | channel: 66 | name: "Salon" 67 | description: "Activer/Désactiver dans un salon" 68 | content: "**Activer/Désactiver %{bot} dans %{channel}**\n- %{state}\n- %{default_state}%{perms}" 69 | toggle: 70 | "true": "Activé" 71 | "false": "Désactivé" 72 | toggle_all: 73 | "true": "Activé partout" 74 | "false": "Désactivé partout" 75 | "none": "Activer/Désactiver partout" 76 | toggle_default: 77 | "true": "Activé pour les nouveaux salons" 78 | "false": "Désactivé pour les nouveaux salons" 79 | "premium": "Changer pour les nouveaux salons (fonctionnalité premium)" 80 | state: 81 | "true": "🟢 Activé dans %{channel}" 82 | "false": "🔴 Désactivé dans %{channel}" 83 | all_state: 84 | "true": "🟢 Activé dans tous les salons" 85 | "false": "🔴 Désactivé dans tous les salons" 86 | default_state: 87 | "true": "🟢 Activé pour les nouveaux salons (par défaut)" 88 | "false": "🔴 Désactivé pour les nouveaux salons (par défaut)" 89 | member: 90 | name: "Membre" 91 | description: "Activer/Désactiver pour un membre" 92 | content: "**Activer/Désactiver %{bot} pour %{member}**\n- %{state}\n- %{default_state}" 93 | toggle: 94 | "true": "Activé" 95 | "false": "Désactivé" 96 | toggle_all: 97 | "true": "Activé pour tous" 98 | "false": "Désactivé pour tous" 99 | "none": "Activer/Désactiver pour tous" 100 | toggle_default: 101 | "true": "Activé pour les nouveaux membres" 102 | "false": "Désactivé pour les nouveaux membres" 103 | premium: "Changer pour les nouveaux membres (fonctionnalité premium)" 104 | state: 105 | "true": "🟢 Activé pour %{member}" 106 | "false": "🔴 Désactivé pour %{member}" 107 | all_state: 108 | "true": "🟢 Activé pour tout le monde" 109 | "false": "🔴 Désactivé pour tout le monde" 110 | default_state: 111 | "true": "🟢 Activé pour les nouveaux membres (par défaut)" 112 | "false": "🔴 Désactivé pour les nouveaux membres (par défaut)" 113 | role: 114 | name: "Rôle" 115 | description: "Activer/Désactiver pour un rôle" 116 | content: "**Activer/Désactiver %{bot} pour %{role}**\n- %{state}\n- %{default_state}" 117 | toggle: 118 | "true": "Activé" 119 | "false": "Désactivé" 120 | toggle_all: 121 | "true": "Activé pour tous les rôles" 122 | "false": "Désactivé pour tous les rôles" 123 | "none": "Activer/Désactiver pour tous les rôles" 124 | toggle_default: 125 | "true": "Activé pour les nouveaux rôles" 126 | "false": "Désactivé pour les nouveaux rôles" 127 | premium: "Changer pour les nouveaux rôles (fonctionnalité premium)" 128 | state: 129 | "true": "🟢 Activé pour %{role}" 130 | "false": "🔴 Désactivé pour %{role}" 131 | all_state: 132 | "true": "🟢 Activé pour tous les rôles" 133 | "false": "🔴 Désactivé pour tous les rôles" 134 | default_state: 135 | "true": "🟢 Activé pour les nouveaux rôles (par défaut)" 136 | "false": "🔴 Désactivé pour les nouveaux rôles (par défaut)" 137 | reply_method: 138 | name: "Méthode de réponse" 139 | description: "Changer le comportement de réponse" 140 | content: "**Changer ce qu'il faut faire sur la réponse**\n- %{state}\n- %{silent}%{perms}" 141 | reply: 142 | button: 143 | "true": "Répondre" 144 | "false": "Envoyer" 145 | state: 146 | "true": "%{emoji} Répondre aux messages" 147 | "false": "📨 Juste envoyer le message" 148 | silent: 149 | button: 150 | "true": "Silencieux" 151 | "false": "Avec notification" 152 | state: 153 | "true": "🔕 Les messages seront envoyés silencieusement" 154 | "false": "🔔 Les messages seront envoyés avec notification" 155 | webhooks: 156 | name: "Webhooks" 157 | description: "Activer/Désactiver pour les webhooks" 158 | content: "**Changer le comportement sur les webhooks**\n%{state}" 159 | button: 160 | "true": "Répondre" 161 | "false": "Ignorer" 162 | state: 163 | "true": "🟢 Répondre aux webhooks" 164 | "false": "🔴 Ignorer les webhooks" 165 | original_message: 166 | name: "Message original" 167 | description: "Change le comportement sur le message original" 168 | content: "**Changer ce qu'il faut faire sur le message original**\n%{state}%{perms}" 169 | option: 170 | nothing: 171 | label: "Ne rien faire" 172 | emoji: "🚫" 173 | remove_embeds: 174 | label: "Supprimer les intégrations" 175 | emoji: "✂️" 176 | delete: 177 | label: "Supprimer complètement le message" 178 | emoji: "🗑️" 179 | troubleshooting: 180 | name: "Dépannage" 181 | description: "Vérifier l'état du bot et résoudre les problèmes courants" 182 | ping: 183 | name: "Ping" 184 | value: "%{latency} ms" 185 | premium: 186 | name: "Premium" 187 | "true": "✨ Ce serveur a les fonctionnalités premium activées ! ✨" 188 | "false": "Ce serveur n'est pas premium." 189 | permissions: "Permissions dans %{channel}" 190 | options: "Options" 191 | websites: "Sites" 192 | refresh: "Actualiser" 193 | custom_websites: "Sites personnalisés" 194 | websites: 195 | name: "Sites web" 196 | description: "Changer les paramètres des sites" 197 | placeholder: "Sélectionnez un site à éditer" 198 | content: "**Changer les paramètres des sites**\n\nSélectionnez un site web pour éditer ses paramètres" 199 | base_website: 200 | description: "Changer les paramètres des liens %{name}" 201 | content: "**Activer/Désactiver la correction des liens %{name}**\n%{state}%{view}\n-# Crédits : %{credits}" 202 | view: 203 | normal: 204 | label: "Vue normale" 205 | emoji: "🔗" 206 | gallery: 207 | label: "Vue galerie" 208 | emoji: "🖼️" 209 | text_only: 210 | label: "Vue texte uniquement" 211 | emoji: "📝" 212 | direct_media: 213 | label: "Vue média direct" 214 | emoji: "📸" 215 | state: 216 | "true": "🟢 Correction des liens %{name}" 217 | "false": "🔴 Pas de correction des liens %{name}" 218 | button: 219 | "true": "Activé" 220 | "false": "Désactivé" 221 | twitter: 222 | name: "Twitter" 223 | description: "Changer les paramètres des liens Twitter" 224 | content: "**Activer/Désactiver la correction des liens Twitter et gérer les traductions**\n%{state}\n%{view}\n-# Crédits : %{credits}" 225 | state: 226 | "true": "🟢 Correction des liens Twitter" 227 | "false": "🔴 Pas de correction des liens Twitter" 228 | translation: 229 | "true": " et traduction vers '%{lang}'" 230 | "false": " mais pas de traduction" 231 | button: 232 | state: 233 | "true": "Activé" 234 | "false": "Désactivé" 235 | translation: 236 | "true": "Traduction vers '%{lang}'" 237 | "false": "Traduction désactivée" 238 | translation_lang: "Éditer la langue de traduction" 239 | modal: 240 | title: "Éditer la langue de traduction" 241 | label: "Langue de traduction" 242 | placeholder: "Entrez le code de langue ISO 2 lettres de la traduction (ex 'fr')" 243 | error: "'%{lang}' n'est pas une langue valide. Veuillez entrer un code valide de langue ISO 2 lettres (ex 'fr'). [Liste des codes de langue ISO](>)" 244 | custom_websites: 245 | name: "Sites personnalisés" 246 | description: "Ajouter ou supprimer des sites personnalisés à corriger" 247 | content: "**Ajouter ou supprimer des sites personnalisés à corriger**" 248 | list: "\n\nSites enregistrés :\n" 249 | website: "- %{name} : `%{domain}` → `%{fix_domain}`" 250 | selected_website: "- **%{name} : `%{domain}` → `%{fix_domain}`**" 251 | placeholder: "Sélectionnez le site à éditer" 252 | empty: "Aucun site personnalisé enregistré" 253 | button: 254 | add: "Ajouter un site" 255 | edit: "Éditer le site" 256 | delete: "Supprimer le site" 257 | premium: "Ajouter un site (premium au-dessus de 3 sites)" 258 | max: "Ajouter un site (impossible d'ajouter plus de 25 sites)" 259 | modal: 260 | title: "Ajouter un site personnalisé" 261 | name: 262 | label: "Nom du site" 263 | placeholder: "Entrez le nom du site (ex 'Mon site')" 264 | domain: 265 | label: "Nom de domaine du site" 266 | placeholder: "Entrez le nom de domaine du site (ex 'monsite.com')" 267 | fix_domain: 268 | label: "Nom de domaine corrigé" 269 | placeholder: "Entrez le nom de domaine corrigé (ex 'fxmonsite.com')" 270 | error: 271 | exists: "Ce site a déjà une correction" 272 | length_name: "La longueur du nom du site doit être inférieure à %{max} caractères" 273 | length_domain: "La longueur du domaine du site web doit être inférieure à %{max} caractères" 274 | misc: 275 | discord_discovery_description: | 276 | FixTweetBot est un bot Discord qui corrige les embeds des réseaux sociaux, 277 | en utilisant des services en ligne (tels que [FxTwitter](https://github.com/FixTweet/FxTwitter)). 278 | 279 | **Concrètement, ce bot réaffiche automatiquement les liens des réseaux sociaux dans une version « corrigée » qui 280 | contient un meilleur embed (qui permet de lire des vidéos directement dans Discord, par exemple).** 281 | 282 | ## Caractéristiques et points forts 283 | 284 | - Prend en charge Twitter, Nitter, Instagram, TikTok, Reddit, Threads, Bluesky, Snapchat, Facebook, Pixiv, Twitch, 285 | Spotify, DeviantArt, Mastodon, Tumblr, BiliBili, IFunny, Fur Affinity, YouTube, et n'importe quel site 286 | personnalisé de votre choix 287 | - Traduction des tweets 288 | - Désactivation par site, salon, membre ou rôle 289 | - Comportement et apparence hautement personnalisables 290 | - Plusieurs langues prises en charge 291 | - Interface moderne pour les paramètres 292 | - Peut répondre aux bots et aux webhooks 293 | - Respecte le format markdown 294 | - Respect de la vie privée 295 | - Source-available (Code source disponible) 296 | 297 | ## Utilisation 298 | 299 | Envoyez simplement un message contenant un lien de réseau social compatible, et le bot supprimera l'intégration 300 | s'il y en a une et le repostera automatiquement en tant que lien « fixé ». 301 | 302 | Vous pouvez également ignorer un lien en le plaçant entre `<` et `>`, comme ceci : ``. 303 | 304 | Vous pouvez gérer les paramètres du bot avec la commande `/paramètres`. 305 | 306 | Enfin, vous pouvez utiliser la commande `/à-propos` à tout moment pour obtenir plus d'informations sur le bot et 307 | pour obtenir de l'aide. 308 | -------------------------------------------------------------------------------- /locales/hi.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | -------------------------------------------------------------------------------- /locales/hr.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | -------------------------------------------------------------------------------- /locales/hu.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | -------------------------------------------------------------------------------- /locales/id.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | -------------------------------------------------------------------------------- /locales/it.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | about: 3 | help: 4 | name: "Aiuto" 5 | -------------------------------------------------------------------------------- /locales/ja.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | about: 3 | command: 4 | name: "概要" 5 | description: "このボットについての情報を入手する" 6 | name: "概要" 7 | description: "このボットはSNSのリンクを修正バージョンとして自動的に再投稿します" 8 | help: 9 | name: "ヘルプ" 10 | value: "サーバーでBOTの設定をするには `/settings` を使用します。 Botに問題が発生した場合`/settings`コマンドの`トラブルシューティング`セクションを使用してください。より詳しいヘルプは[サポートサーバー](https://discord.gg/3ej9JrkF3U)をご覧ください。" 11 | premium: 12 | name: "Premium" 13 | "true": "✨このサーバーはプレミアム機能が有効になっています! ✨" 14 | "false": "このサーバーはプレミアムが有効になっていません" 15 | invite: "招待" 16 | source: "ソースコード" 17 | support: "サポートサーバー" 18 | links: 19 | name: "リンク" 20 | value: | 21 | - [招待リンク](%{invite_link}) 22 | - [Tog.gg Page](https://top.gg/bot/1164651057243238400)( 投票してください!) 23 | - [ソースコード](%{repo_link})(評価をお願いします!) 24 | - [翻訳プロジェクト](https://crowdin.com/project/fixtweetbot) 25 | (あなたの言語で私達の翻訳を助けてください!) 26 | - [プロキシ/協力者のクレジット](https://github.com/Kyrela/FixTweetBot?tab=readme-ov-file#proxies) 27 | - [サポートサーバー](%{support_link}) 28 | settings: 29 | command: 30 | name: "設定" 31 | description: "FixTweetの設定を管理する" 32 | channel: 33 | name: "キャンセル" 34 | description: "FixTweetの設定を管理するチャンネル" 35 | member: 36 | name: "メンバー" 37 | description: "FixTweet設定を管理するメンバー" 38 | role: 39 | name: "ロール" 40 | description: "FixTweetの設定を管理するロール" 41 | title: "設定" 42 | description: "設定を選択して詳細を表示する" 43 | placeholder: "設定を選択" 44 | perms: 45 | scope: " の%{scope}" 46 | label: "\n\nチャンネルの権限を設定 %{channel}\n" 47 | missing_label: "\n\n**権限がありません**\n" 48 | view_channel: 49 | "true": "🟢 `View channel` の権限があります" 50 | "false": "🔴 `view channel` の権限がありません" 51 | send_messages: 52 | "true": "🟢 `Send message` の権限があります" 53 | "false": "🔴 `send message`の権限がありません" 54 | send_messages_in_threads: 55 | "true": "🟢 `Send message in threads` の権限があります" 56 | "false": "🔴 `send message in threads`の権限がありません" 57 | embed_links: 58 | "true": "🟢 `Embed links` の権限があります" 59 | "false": "🔴`embed links` の権限がありません" 60 | manage_messages: 61 | "true": "🟢 `Manage messages` の権限があります" 62 | "false": "🔴 `manage messages` の権限がありません" 63 | read_message_history: 64 | "true": "🟢 `Read message history` の権限があります" 65 | "false": "🔴`read message history` の権限がありません" 66 | channel: 67 | name: "チャンネル" 68 | description: "このチャンネルで有効/無効" 69 | content: "**有効/無効 %{bot} in %{channel}**\n- %{state}\n- %{default_state}%{perms}" 70 | toggle: 71 | "true": "有効です" 72 | "false": "無効です" 73 | toggle_all: 74 | "true": "どこでも有効です" 75 | "false": "どこでも無効です" 76 | "none": "どこでも 有効/無効 " 77 | toggle_default: 78 | "true": "新規チャンネルで有効" 79 | "false": "新規チャンネルで無効" 80 | "premium": "新規チャンネルで変更(将来的なプレミアム機能)" 81 | state: 82 | "true": "🟢 %{channel} で有効" 83 | "false": "🔴%{channel} で無効です" 84 | all_state: 85 | "true": "🟢 すべてのチャンネルで有効" 86 | "false": "🔴 全てのチャンネルで無効" 87 | default_state: 88 | "true": "🟢新規チャンネルで有効 (デフォルト)" 89 | "false": "🔴 新規チャンネルで無効 (デフォルト)" 90 | member: 91 | name: "メンバー" 92 | description: "メンバーの有効/無効" 93 | content: "**有効/無効 %{bot} for %{member}**\n- %{state}\n- %{default_state}" 94 | toggle: 95 | "true": "有効" 96 | "false": "無効" 97 | toggle_all: 98 | "true": "誰でも有効" 99 | "false": "全員が無効" 100 | "none": "全員が有効/無効" 101 | toggle_default: 102 | "true": "新規メンバーの有効" 103 | "false": "新規メンバーの無効" 104 | premium: "新規メンバーが変更できる (将来的なプレミアム機能)" 105 | state: 106 | "true": "🟢%{member} が有効" 107 | "false": "🔴%{member} が無効" 108 | all_state: 109 | "true": "🟢全員が有効" 110 | "false": "🔴全員が無効" 111 | default_state: 112 | "true": "🟢 新規メンバーが有効 (デフォルト)" 113 | "false": "🔴 新規メンバーが無効 (デフォルト)" 114 | role: 115 | name: "ロール" 116 | description: "ロール上で有効/無効" 117 | content: "**有効/無効 %{bot} for %{role}**\n- %{state}\n- %{default_state}" 118 | toggle: 119 | "true": "有効" 120 | "false": "無効" 121 | toggle_all: 122 | "true": "すべてのロールで有効" 123 | "false": "すべてのロールで無効" 124 | "none": "すべてのロールで有効/無効" 125 | toggle_default: 126 | "true": "新しいロールで有効" 127 | "false": "新しいロールで無効" 128 | premium: "新しいロールで変更(将来的なプレミアム機能)" 129 | state: 130 | "true": "🟢 %{role} で有効" 131 | "false": "🔴 %{role} で無効" 132 | all_state: 133 | "true": "🟢 すべてのロールで有効" 134 | "false": "🔴 すべてのロールで無効" 135 | default_state: 136 | "true": "🟢 新しいロールで有効 (デフォルト)" 137 | "false": "🔴 新しいロールで無効 (デフォルト)" 138 | reply_method: 139 | name: "リプライの方式" 140 | description: "返信時の動作を変更する" 141 | content: "**返信の内容を変更する**\n- %{state}\n- %{silent}%{perms}" 142 | reply: 143 | button: 144 | "true": "返信しています" 145 | "false": "送信しています" 146 | state: 147 | "true": "%{emoji} メッセージへの返信" 148 | "false": "📨 メッセージを送信中" 149 | webhooks: 150 | button: 151 | "false": "無視" 152 | original_message: 153 | name: "オリジナルのメッセージ" 154 | description: "元のメッセージの動作を変更する" 155 | content: "元のメッセージに対する動作を変更する: %{state}%{perms}" 156 | option: 157 | nothing: 158 | label: "何もしない" 159 | emoji: "🚫" 160 | remove_embeds: 161 | label: "Remove the embeds" 162 | emoji: "✂️" 163 | delete: 164 | label: "メッセージを完全に削除" 165 | emoji: "🗑️" 166 | troubleshooting: 167 | name: "トラブルシューティング" 168 | description: "ボットの状態を確認し、よくある問題を解決する" 169 | ping: 170 | name: "Ping" 171 | value: "%{latency} ms" 172 | premium: 173 | name: "Premium" 174 | "true": "✨ このサーバーではプレミアム機能が有効です! ✨" 175 | "false": "このサーバーではプレミアム機能が有効ではありません" 176 | permissions: "%{channel} での権限" 177 | options: "オプション" 178 | websites: "Webサイト" 179 | refresh: "リフレッシュ" 180 | custom_websites: "カスタムWebサイト" 181 | websites: 182 | name: "Webサイト" 183 | description: "ウェブサイト毎の設定を変更する" 184 | placeholder: "編集するウェブサイトを選択してください" 185 | content: "**異なるウェブサイトの設定を変更**\n\n設定を編集するウェブサイトを選択してください" 186 | base_website: 187 | description: "%{name} のリンク設定を変更する" 188 | content: "**%{name} のリンク修正を有効化/無効化**\n\n%{state}%{view}\n-# クレジット: %{credits}" 189 | view: 190 | normal: 191 | label: "通常ビュー" 192 | emoji: "🔗" 193 | gallery: 194 | label: "ギャラリービュー" 195 | emoji: "🖼️" 196 | text_only: 197 | label: "テキストのみ" 198 | emoji: "📝" 199 | direct_media: 200 | label: "メディアの直リンク" 201 | emoji: "📸" 202 | state: 203 | "true": "🟢%{name} のリンクを修正する" 204 | "false": "🔴 %{name} のリンクを修正しない" 205 | button: 206 | "true": "有効" 207 | "false": "無効" 208 | twitter: 209 | name: "Twitter" 210 | description: "Change the Twitter links settings" 211 | content: "**Twitterのリンク修正を有効化/無効化し、翻訳を管理する**\n%{state}\n%{view}\n-#クレジット: %{credits}" 212 | state: 213 | "true": "🟢 Twitterのリンクを修正する" 214 | "false": "🔴 Twitter のリンクを修正しない" 215 | translation: 216 | "true": " '%{lang}'に翻訳する" 217 | "false": " 翻訳しない" 218 | button: 219 | state: 220 | "true": "有効" 221 | "false": "無効" 222 | translation: 223 | "true": "'%{lang}' に翻訳する" 224 | "false": "翻訳を無効化" 225 | translation_lang: "翻訳する言語を編集" 226 | modal: 227 | title: "翻訳する言語を編集" 228 | label: "翻訳する言語" 229 | placeholder: "翻訳の\"2\"文字のISO言語コードを入力してください (例: 'ja')" 230 | error: "%{lang} は有効な言語ではありません、有効な\"2\"文字のISO言語コードを入力してください(例:'ja')。\n[ISO言語コードの一覧]()" 231 | custom_websites: 232 | name: "カスタムウェブサイト" 233 | description: "カスタムウェブサイトを追加または削除して修正する" 234 | content: "**カスタムウェブサイトを追加または削除して修正する**" 235 | list: "\n\n登録済みのウェブサイト:\n" 236 | website: "- %{name}: `%{domain}` → `%{fix_domain}`" 237 | selected_website: "- **%{name}: `%{domain}` → `%{fix_domain}`**" 238 | placeholder: "編集するウェブサイトを選択してください" 239 | empty: "登録されたウェブサイトはありません" 240 | button: 241 | add: "ウェブサイトを追加" 242 | edit: "ウェブサイトを編集" 243 | delete: "ウェブサイトを削除" 244 | premium: "ウェブサイトを追加(プレミアムは\"3\"つ以上のウェブサイト)" 245 | modal: 246 | title: "カスタムウェブサイトを追加" 247 | name: 248 | label: "ウェブサイトの名前" 249 | placeholder: "ウェブサイト名を入力してください(例:'My website')" 250 | domain: 251 | label: "Webサイトのドメイン" 252 | placeholder: "ウェブサイトのドメインを入力してください(例: 'mywebsite.com')" 253 | fix_domain: 254 | label: "ドメインを修正する" 255 | placeholder: "固定ドメインを入力してください (例: 'fxmywebsite.com')" 256 | error: 257 | exists: "このウェブサイトはすでに修正されています" 258 | -------------------------------------------------------------------------------- /locales/ko.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | -------------------------------------------------------------------------------- /locales/lt.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | -------------------------------------------------------------------------------- /locales/nl.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | -------------------------------------------------------------------------------- /locales/no.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | -------------------------------------------------------------------------------- /locales/pl.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | -------------------------------------------------------------------------------- /locales/pr-BR.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | about: 3 | command: 4 | name: "sobre" 5 | description: "Ver informações sobre o bot" 6 | name: "Sobre" 7 | description: "Este bot reposta automaticamente links de redes sociais como uma versão 'consertada'." 8 | help: 9 | name: "Ajuda" 10 | value: "Use `/settings` para configurar o bot no seu servidor. Se você encontrar problemas com o bot, use a seção `Solução de problemas` do comando `/settings`. Visite o [servidor de suporte](https://discord.gg/3ej9JrkF3U) para receber mais ajuda." 11 | premium: 12 | name: "Premium" 13 | "true": "✨ Este servidor tem recursos premium ativados! ✨" 14 | "false": "Este servidor não é premium." 15 | invite: "Adicionar" 16 | source: "Código-fonte" 17 | support: "Servidor de suporte" 18 | links: 19 | name: "Links" 20 | value: | 21 | - [Adicionar o bot](%{invite_link}) 22 | - [Página do Tog.gg](https://top.gg/bot/1164651057243238400) (por favor vote!) 23 | - [Código-fonte](%{repo_link}) (por favor deixe uma estrela!) 24 | - [Projeto de tradução](https://crowdin.com/project/fixtweetbot) (nos ajude a traduzir no seu idioma!) 25 | - [Créditos dos proxies/consertadores](https://github.com/Kyrela/FixTweetBot?tab=readme-ov-file#proxies) 26 | - [Servidor de suporte](%{support_link}) 27 | settings: 28 | command: 29 | name: "configurações" 30 | description: "Gerenciar configurações do FixTweet" 31 | channel: 32 | name: "canal" 33 | description: "Gerenciar configurações do FixTweet para um canal" 34 | member: 35 | name: "membro" 36 | description: "Gerenciar configurações do FixTweet para um membro" 37 | role: 38 | name: "cargo" 39 | description: "Gerenciar configurações do FixTweet para um cargo" 40 | title: "Configurações" 41 | description: "Escolha uma configuração para ver os detalhes" 42 | placeholder: "Escolha uma configuração" 43 | perms: 44 | scope: " em %{scope}" 45 | label: "\n\nPermissões em %{channel}:\n" 46 | missing_label: "\n\n**Permissões faltando:**\n" 47 | view_channel: 48 | "true": "🟢 Permissão de `Ver canal`" 49 | "false": "🔴 Faltando permissão de `Ver canal`" 50 | send_messages: 51 | "true": "🟢 Permissão de `Enviar mensagens`" 52 | "false": "🔴 Faltando permissão de `Enviar mensagens`" 53 | send_messages_in_threads: 54 | "true": "🟢 Permissão de `Enviar mensagens em tópicos`" 55 | "false": "🔴 Faltando permissão de `Enviar mensagens em tópicos`" 56 | embed_links: 57 | "true": "🟢 Permissão de `Inserir links`" 58 | "false": "🔴 Faltando permissão de `Inserir links`" 59 | manage_messages: 60 | "true": "🟢 Permissão de `Gerenciar mensagens`" 61 | "false": "🔴 Faltando permissão de `Gerenciar mensagens`" 62 | read_message_history: 63 | "true": "🟢 Permissão de `Ver histórico de mensagens`" 64 | "false": "🔴 Faltando permissão de `Ver histórico de mensagens`" 65 | channel: 66 | name: "Canal" 67 | description: "Ativar/Desativar em um canal" 68 | content: "**Ativar/Desativar %{bot} em %{channel}**\n- %{state}\n- %{default_state}%{perms}" 69 | toggle: 70 | "true": "Ativado" 71 | "false": "Desativado" 72 | toggle_all: 73 | "true": "Ativar em todos os canais" 74 | "false": "Desativar em todos os canais" 75 | "none": "Ativar/Desativar em todos os canais" 76 | toggle_default: 77 | "true": "Ativado para novos canais" 78 | "false": "Desativado para novos canais" 79 | "premium": "Alterar para novos canais (recurso premium)" 80 | state: 81 | "true": "🟢 Ativado em %{channel}" 82 | "false": "🔴 Desativado em %{channel}" 83 | all_state: 84 | "true": "🟢 Ativado em todos os canais" 85 | "false": "🔴 Desativado em todos os canais" 86 | default_state: 87 | "true": "🟢 Ativado para novos canais (por padrão)" 88 | "false": "🔴 Desativado para novos canais (por padrão)" 89 | member: 90 | name: "Membro" 91 | description: "Ativar/Desativar para um membro" 92 | content: "**Ativar/Desativar %{bot} para %{member}**\n- %{state}\n- %{default_state}" 93 | toggle: 94 | "true": "Ativado" 95 | "false": "Desativado" 96 | toggle_all: 97 | "true": "Ativado para todos" 98 | "false": "Desativado para todos" 99 | "none": "Ativar/Desativar para todos" 100 | toggle_default: 101 | "true": "Ativado para novos membros" 102 | "false": "Desativado para novos membros" 103 | premium: "Alterar para novos membros (recurso premium)" 104 | state: 105 | "true": "🟢 Ativado para %{member}" 106 | "false": "🔴 Desativado para %{member}" 107 | all_state: 108 | "true": "🟢 Ativado para todos" 109 | "false": "🔴 Desativado para todos" 110 | default_state: 111 | "true": "🟢 Ativado para novos membros (por padrão)" 112 | "false": "🔴 Desativado para novos membros (por padrão)" 113 | role: 114 | name: "Cargo" 115 | description: "Ativar/Desativar para um cargo" 116 | content: "**Ativar/Desativar %{bot} para %{role}**\n- %{state}\n- %{default_state}" 117 | toggle: 118 | "true": "Ativado" 119 | "false": "Desativado" 120 | toggle_all: 121 | "true": "Ativado para todos os cargos" 122 | "false": "Desativado para todos os cargos" 123 | "none": "Ativado/Desativado para todos os cargos" 124 | toggle_default: 125 | "true": "Ativado para novos cargos" 126 | "false": "Desativado para novos cargos" 127 | premium: "Alterar para novos cargos (recurso premium)" 128 | state: 129 | "true": "🟢 Ativado para %{role}" 130 | "false": "🔴 Desativado para %{role}" 131 | all_state: 132 | "true": "🟢 Ativado para todos os cargos" 133 | "false": "🔴 Desativado para todos os cargos" 134 | default_state: 135 | "true": "🟢 Ativado para novos cargos (por padrão)" 136 | "false": "🔴 Desativado para novos cargos (por padrão)" 137 | reply_method: 138 | name: "Método de resposta" 139 | description: "Alterar o comportamento da resposta" 140 | content: "**Alterar o que fazer ao responder**\n- %{state}\n- %{silent}%{perms}" 141 | reply: 142 | button: 143 | "true": "Respondendo" 144 | "false": "Enviando" 145 | state: 146 | "true": "%{emoji} Respondendo às mensagens" 147 | "false": "📨 Apenas enviando a mensagem" 148 | webhooks: 149 | name: "**Alterar o comportamento em webhooks**" 150 | description: "Ativar/Desativar para webhooks" 151 | content: "**Alterar o comportamento em webhooks**\n%{state}" 152 | button: 153 | "true": "Respondendo" 154 | "false": "Ignorando" 155 | state: 156 | "true": "🟢 Respondendo webhooks" 157 | "false": "🔴 Ignorando webhooks" 158 | original_message: 159 | name: "Mensagem original" 160 | description: "Alterar o comportamento na mensagem original" 161 | content: "**Alterar o que fazer com a mensagem original**\n%{state}%{perms}" 162 | option: 163 | nothing: 164 | label: "Não fazer nada" 165 | emoji: "🚫" 166 | remove_embeds: 167 | label: "Remover o conteúdo integrado" 168 | emoji: "✂️" 169 | delete: 170 | label: "Excluir a mensagem completamente" 171 | emoji: "🗑️" 172 | troubleshooting: 173 | name: "Solução de problemas" 174 | description: "Verifique o status do bot e soluções de problemas comuns" 175 | ping: 176 | name: "Latência" 177 | value: "%{latency} ms" 178 | premium: 179 | name: "Premium" 180 | "true": "✨ Este servidor tem recursos premium ativados! ✨" 181 | "false": "Este servidor não é premium." 182 | permissions: "Permissões em %{channel}" 183 | options: "Opções" 184 | websites: "Sites" 185 | refresh: "Recarregar" 186 | custom_websites: "Sites personalizados" 187 | websites: 188 | name: "Sites" 189 | description: "Alterar as configurações de diferentes sites" 190 | placeholder: "Selecionar um site para editar" 191 | content: "**Alterar as configurações de diferentes sites**\n\nSelecionar um site para editar suas configurações" 192 | base_website: 193 | description: "Alterar as configurações de links de %{name}" 194 | content: "**Ativar/Desativar a correção de links de %{name}**\n%{state}%{view}\n-# Créditos: %{credits}" 195 | view: 196 | normal: 197 | label: "Visualização normal" 198 | emoji: "🔗" 199 | gallery: 200 | label: "Visualização de galeria" 201 | emoji: "🖼️" 202 | text_only: 203 | label: "Visualização de apenas texto" 204 | emoji: "📝" 205 | direct_media: 206 | label: "Visualização de mídia direta" 207 | emoji: "📸" 208 | state: 209 | "true": "🟢 Consertando links de %{name}" 210 | "false": "🔴 Não consertando links de %{name}" 211 | button: 212 | "true": "Ativado" 213 | "false": "Desativado" 214 | twitter: 215 | name: "Twitter" 216 | description: "Alterar as configurações de links de Twitter" 217 | content: "**Ativar/Desativar o conserto de links de Twitter e gerenciar traduções**\n%{state}\n%{view}\n-# Créditos: %{credits}" 218 | state: 219 | "true": "🟢 Consertando links de Twitter" 220 | "false": "🔴 Não consertando links de Twitter" 221 | translation: 222 | "true": " e os traduzindo para '%{lang}'" 223 | "false": " mas não traduzindo" 224 | button: 225 | state: 226 | "true": "Ativado" 227 | "false": "Desativado" 228 | translation: 229 | "true": "Traduzindo para '%{lang}'" 230 | "false": "Traduções desativadas" 231 | translation_lang: "Editar idioma de tradução" 232 | modal: 233 | title: "Editar idioma de tradução" 234 | label: "Idioma de tradução" 235 | placeholder: "Escreva o código de 2 letras ISO do idioma de tradução (ex: 'pt')" 236 | error: "'%{lang}' não é um idioma válido. Por favor, escreva um código de 2 letras ISO válido (ex: 'pt'). [Lista de códigos de idiomas ISO]()" 237 | custom_websites: 238 | name: "Sites personalizados" 239 | description: "Adicionar ou remover sites personalizados para consertar" 240 | content: "**Adicionar ou remover sites personalizados para consertar**" 241 | list: "\n\nSites registrados:\n" 242 | website: "- %{name}: `%{domain}` → `%{fix_domain}`" 243 | selected_website: "- **%{name}: `%{domain}` → `%{fix_domain}`**" 244 | placeholder: "Selecionar o site para editar" 245 | empty: "Nenhum site registrado" 246 | button: 247 | add: "Adicionar site" 248 | edit: "Editar site" 249 | delete: "Remover site" 250 | premium: "Adicionar site (premium acima de 3 sites)" 251 | max: "Adicionar site (não é possível adicionar mais de 25 sites)" 252 | modal: 253 | title: "Adicionar um site personalizado" 254 | name: 255 | label: "Nome do site" 256 | placeholder: "Escreva o nome do site (ex: 'Meu site')" 257 | domain: 258 | label: "Domínio do site" 259 | placeholder: "Escreva o domínio do site (ex: 'meusite.com')" 260 | fix_domain: 261 | label: "Domínio consertador" 262 | placeholder: "Escreva o nome consertado (ex: 'fxmeusite.com')" 263 | error: 264 | exists: "Este site já tem um conserto" 265 | length_name: "O comprimento do nome do site deve ser inferior a %{max} caracteres" 266 | length_domain: "O comprimento do domínio do site deve ser inferior a %{max} caracteres" 267 | misc: 268 | discord_discovery_description: | 269 | FixTweetBot é um bot Discord que corrige incorporações de mídia social, usando serviços online (como [FxTwitter](https://github.com/FixTweet/FxTwitter)) 270 | 271 | **Em termos concretos, este bot repassa automaticamente links de mídia social como uma versão ‘fixa’ que contém uma incorporação melhor (que permite reproduzir vídeos diretamente no Discord, por exemplo).** 272 | 273 | ## Recursos e destaques 274 | 275 | - Suporta Twitter, Nitter, Instagram, TikTok, Reddit, Threads, Bluesky, Snapchat, Facebook, Pixiv, Deviantart, Mastodon, Tumblr, BiliBili, IFunny, Fur Affinity, YouTube e qualquer site personalizado de sua escolha 276 | - Tradução de tweets 277 | - Desativar por site, canal, membro ou função 278 | - Comportamento e aparência altamente personalizáveis 279 | - Vários idiomas suportados 280 | - Interface moderna para configurações 281 | - Respeite a remarcação 282 | - Respeite a privacidade 283 | - Fonte disponível 284 | 285 | ## Uso 286 | 287 | Basta enviar uma mensagem contendo um link de mídia social compatível e o bot removerá a incorporação, se houver, e repostá-lo automaticamente como um link 'fixo'. 288 | 289 | Você também pode ignorar um link colocando-o entre `<` e `>`, assim: ``. 290 | 291 | Você pode gerenciar as configurações do bot com o comando `/settings`. 292 | 293 | Por último, você pode usar o comando `/about` a qualquer momento para obter mais informações sobre o bot e obter ajuda. 294 | -------------------------------------------------------------------------------- /locales/ro.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | -------------------------------------------------------------------------------- /locales/ru.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | -------------------------------------------------------------------------------- /locales/sv-SE.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | -------------------------------------------------------------------------------- /locales/th.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | -------------------------------------------------------------------------------- /locales/tr.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | about: 3 | command: 4 | name: "hakkında" 5 | description: "Bot hakkında bilgi alın" 6 | name: "Hakkında" 7 | description: "Bu bot otomatik olarak sosyal medya bağlantılarını 'düzeltilmiş' olarak gönderir." 8 | help: 9 | name: "Yardım" 10 | value: "Sunucunuzdaki botu özelleştirmek için `/ayarlar` komutunu kullanın. Eğer bot ile bir sorun yaşarsanız `/ayarlar` komutundaki `Sorun giderme` seçeneğini seçin. Daha fazla yardım için [destek sunucusunu](https://discord.gg/3ej9JrkF3U) ziyaret edin." 11 | premium: 12 | name: "Premium" 13 | "true": "✨ Bu sunucuda premium özellikleri aktif! ✨" 14 | "false": "Bu sunucu premium değil." 15 | invite: "Davet" 16 | source: "Kaynak kodu" 17 | support: "Destek sunucusu" 18 | links: 19 | name: "Bağlantılar" 20 | value: | 21 | - [Davet bağlantısı](%{invite_link}) 22 | - [Tog.gg sayfası](https://top.gg/bot/1164651057243238400) (lütfen oy verin!) 23 | - [Kaynak kodu](%{repo_link}) (lütfen yıldızlayın!) 24 | - [Çeviri projesi](https://crowdin.com/project/fixtweetbot) (botu diğer dillere çevirmemize yardımcı olun!) 25 | - [Emeği geçenler](https://github.com/Kyrela/FixTweetBot?tab=readme-ov-file#proxies) 26 | - [Destek sunucusu](%{support_link}) 27 | settings: 28 | command: 29 | name: "ayarlar" 30 | description: "FixTweet ayarlarını yönet" 31 | channel: 32 | name: "kanal" 33 | description: "FixTweet ayarlarının yönetildiği kanal" 34 | member: 35 | name: "üye" 36 | description: "FixTweet ayarlarını yönetebilecek üye" 37 | role: 38 | name: "rol" 39 | description: "FixTweet ayarlarını yönetebilecek rol" 40 | title: "Ayarlar" 41 | description: "Detaylarını görüntülemek için bir ayar seçin" 42 | placeholder: "Bir ayar seçin" 43 | perms: 44 | scope: " %{scope} içinde" 45 | label: "\n\n%{channel} daki yetkiler:\n" 46 | missing_label: "\n\n**Eksik yetkiler:**\n" 47 | view_channel: 48 | "true": "🟢 `Kanalı görüntüle` izini" 49 | "false": "🔴 `Kanalı görüntüle` izini bulunamadı" 50 | send_messages: 51 | "true": "🟢 `Mesaj gönderme` izini" 52 | "false": "🔴 `Mesaj gönderme` izini yok" 53 | channel: 54 | name: "Kanal" 55 | description: "Bir kanalda etkinleştir/devre dışı bırak" 56 | content: "**%{bot} botunu %{channel} kanalında etkinleştir/devre dışı bırak**\n- %{state}\n- %{default_state}%{perms}" 57 | toggle: 58 | "true": "Etkin" 59 | "false": "Devre dışı" 60 | toggle_all: 61 | "true": "Her yerde etkin" 62 | "false": "Her yerde devre dışı" 63 | "none": "Her yerde etkinleştir/devre dışı bırak" 64 | toggle_default: 65 | "true": "Yeni kanallar için etkin" 66 | "false": "Yeni kanallar için devre dışı" 67 | "premium": "Yeni kanallar için değiştir (premium özellik)" 68 | state: 69 | "true": "🟢 %{channel} kanalında etkin" 70 | "false": "🔴 %{channel} kanalında devre dışı" 71 | all_state: 72 | "true": "🟢 Bütün kanallarda etkin" 73 | "false": "🔴 Bütün kanallarda devre dışı" 74 | member: 75 | name: "Üye" 76 | description: "Bir üyede botu etkinleştir/devre dışı bırak" 77 | content: "**%{bot} botunu %{member} için etkinleştir/devre dışı bırak**\n- %{state}\n- %{default_state}" 78 | toggle: 79 | "true": "Etkin" 80 | "false": "Devre dışı" 81 | toggle_all: 82 | "true": "Herkes için etkin" 83 | "false": "Herkes için devre dışı" 84 | "none": "Herkes için etkinleştir/devre dışı bırak" 85 | toggle_default: 86 | "true": "Yeni üyeler için etkin" 87 | "false": "Yeni üyeler için devre dışı" 88 | premium: "Yeni üyeler için değiştir (premium özellik)" 89 | state: 90 | "true": "🟢 %{member} için etkin" 91 | "false": "🔴 %{member} için devre dışı" 92 | all_state: 93 | "true": "🟢 Herkes için etkin" 94 | "false": "🔴 Herkes için devre dışı" 95 | role: 96 | name: "Rol" 97 | description: "Rolde Etkinleştir/Devre dışı bırak" 98 | content: "**%{bot} botunu %{role} rolü için etkinleştir/devre dışı bırak**\n- %{state}\n- %{default_state}" 99 | toggle: 100 | "true": "Etkin" 101 | "false": "Devre dışı" 102 | toggle_all: 103 | "true": "Her rol için etkin" 104 | "false": "Her rol için devre dışı" 105 | toggle_default: 106 | "true": "Yeni roller için etkin" 107 | "false": "Yeni roller için devre dışı" 108 | reply_method: 109 | reply: 110 | button: 111 | "true": "Yanıt veriyor" 112 | "false": "Gönderiyor" 113 | state: 114 | "true": "%{emoji} Mesajlara yanıt veriyor" 115 | "false": "📨 Sadece mesajı gönderiyor" 116 | silent: 117 | button: 118 | "true": "Sessiz" 119 | "false": "Bildirimli" 120 | state: 121 | "true": "🔕 Mesajlar sessizce gönderilecek" 122 | "false": "🔔 Mesajlar bildirim ile gönderilecek" 123 | original_message: 124 | name: "Orijinal mesaj" 125 | option: 126 | nothing: 127 | label: "Bir şey yapma" 128 | emoji: "🚫" 129 | remove_embeds: 130 | emoji: "✂️" 131 | delete: 132 | emoji: "🗑️" 133 | troubleshooting: 134 | name: "Sorun giderme" 135 | ping: 136 | name: "Ping" 137 | value: "%{latency} ms" 138 | premium: 139 | name: "Premium" 140 | "true": "✨ Bu sunucuda premium özellikler etkin! ✨" 141 | "false": "Bu sunucu premium değil." 142 | permissions: "%{channel} kanalında yetkiler" 143 | options: "Ayarlar" 144 | websites: "Websiteler" 145 | refresh: "Yenile" 146 | base_website: 147 | view: 148 | gallery: 149 | label: "Galeri görünümü" 150 | emoji: "🖼️" 151 | text_only: 152 | label: "Yalnızca metni görüntüleme" 153 | emoji: "📝" 154 | direct_media: 155 | emoji: "📸" 156 | state: 157 | "true": "🟢 %{name} bağlantılarını düzeltiyor" 158 | "false": "🔴 %{name} bağlantılarını düzeltmiyor" 159 | button: 160 | "true": "Etkin" 161 | "false": "Devre Dışı" 162 | twitter: 163 | name: "Twitter" 164 | state: 165 | "true": "🟢 Twitter bağlantılarını düzeltiyor" 166 | "false": "🔴 Twitter bağlantılarını düzeltmiyor" 167 | button: 168 | state: 169 | "true": "Etkin" 170 | "false": "Devre dışı" 171 | translation: 172 | "true": "'%{lang}' diline çevriliyor" 173 | "false": "Çeviriler kapalı" 174 | translation_lang: "Çeviri dilini düzenle" 175 | modal: 176 | title: "Çeviri dilini düzenle" 177 | label: "Çeviri dili" 178 | custom_websites: 179 | list: "\n\nKayıtlı siteler:\n" 180 | button: 181 | add: "Web sitesi ekle" 182 | edit: "Web sitesini düzenle" 183 | delete: "Web sitesi kaldır" 184 | -------------------------------------------------------------------------------- /locales/uk.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | -------------------------------------------------------------------------------- /locales/vi.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | -------------------------------------------------------------------------------- /locales/zh-CN.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | about: 3 | command: 4 | name: "关于" 5 | description: "取得关于机器人的资讯" 6 | name: "关于" 7 | description: "这个机器人会自动将社群媒体连结重新发佈为「修復的」版本。" 8 | help: 9 | name: "帮助" 10 | value: "使用 `/设定` 在你的群组中设定机器人。如果你在使用机器人时遇到问题,请使用 `/设定` 指令中的 `故障排除` 功能。加入 [支援伺服器](https://discord.gg/3ej9JrkF3U) 以取得更多协助。" 11 | premium: 12 | name: "会员" 13 | "true": "✨ 这个伺服器已启用会员功能!✨" 14 | "false": "这个伺服器没有会员。" 15 | invite: "邀请" 16 | source: "原始码" 17 | support: "支援伺服器" 18 | links: 19 | name: "连结" 20 | value: | 21 | - [邀请连结](%{invite_link}) 22 | - [Tog.gg 页面](https://top.gg/bot/1164651057243238400)(请投票!) 23 | - [原始码](%{repo_link})(请留下星星!) 24 | - [翻译专案](https://crowdin.com/project/fixtweetbot)(协助我们翻译成你的语言!) 25 | - [代理/修復程式积分](https://github.com/Kyrela/FixTweetBot?tab=readme-ov-file#proxies) 26 | - [支援伺服器](%{support_link}) 27 | settings: 28 | command: 29 | name: "设定" 30 | description: "管理 FixTweet 的设定" 31 | channel: 32 | name: "频道" 33 | description: "管理 FixTweet 设定的频道" 34 | member: 35 | name: "成员" 36 | description: "管理 FixTweet 设定的成员" 37 | role: 38 | name: "身分组" 39 | description: "管理 FixTweet 设定的身分组" 40 | title: "设定" 41 | description: "选择一个设定以查看其详细资讯" 42 | placeholder: "选择一个设定" 43 | perms: 44 | scope: " 在 %{scope}" 45 | label: "\n\n在 %{channel} 的权限:\n" 46 | missing_label: "\n\n**缺少的权限:**\n" 47 | view_channel: 48 | "true": "🟢 `检视频道` 权限" 49 | "false": "🔴 缺少 `检视频道` 权限" 50 | send_messages: 51 | "true": "🟢 `发送讯息` 权限" 52 | "false": "🔴 缺少 `发送讯息` 权限" 53 | send_messages_in_threads: 54 | "true": "🟢 `在讨论串中传送讯息` 权限" 55 | "false": "🔴 缺少 `在讨论串中传送讯息` 权限" 56 | embed_links: 57 | "true": "🟢 `嵌入连结` 权限" 58 | "false": "🔴 缺少 `嵌入连结` 权限" 59 | manage_messages: 60 | "true": "🟢 `管理讯息` 权限" 61 | "false": "🔴 缺少 `管理讯息` 权限" 62 | read_message_history: 63 | "true": "🟢 `读取讯息历史` 权限" 64 | "false": "🔴 缺少 `读取讯息历史` 权限" 65 | channel: 66 | name: "频道" 67 | description: "在一个频道上启用/停用" 68 | content: "**在 %{channel} 内启用/停用 %{bot}**\n- %{state}\n- %{default_state}%{perms}" 69 | toggle: 70 | "true": "启用" 71 | "false": "停用" 72 | toggle_all: 73 | "true": "在所有地方启用" 74 | "false": "在所有地方停用" 75 | "none": "在所有地方启用/停用" 76 | toggle_default: 77 | "true": "为新频道启用" 78 | "false": "为新频道停用" 79 | "premium": "新频道的更改(会员功能)" 80 | state: 81 | "true": "🟢 在 %{channel} 启用" 82 | "false": "🔴 在 %{channel} 停用" 83 | all_state: 84 | "true": "🟢 在所有频道启用" 85 | "false": "🔴 在所有频道停用" 86 | default_state: 87 | "true": "🟢 为新频道启用(预设)" 88 | "false": "🔴 为新频道停用(预设)" 89 | member: 90 | name: "成员" 91 | description: "为一个成员启用/停用" 92 | content: "**为 %{member} 启用/停用 %{bot}**\n- %{state}\n- %{default_state}" 93 | toggle: 94 | "true": "启用" 95 | "false": "停用" 96 | toggle_all: 97 | "true": "为所有人启用" 98 | "false": "为所有人停用" 99 | "none": "为所有人启用/停用" 100 | toggle_default: 101 | "true": "为新成员启用" 102 | "false": "为新成员停用" 103 | premium: "新成员的更改(会员功能)" 104 | state: 105 | "true": "🟢 为 %{member} 启用" 106 | "false": "🔴 为 %{member} 停用" 107 | all_state: 108 | "true": "🟢 为所有人启用" 109 | "false": "🔴 为所有人停用" 110 | default_state: 111 | "true": "🟢 为新成员启用(预设)" 112 | "false": "🔴 为新成员停用(预设)" 113 | role: 114 | name: "身分组" 115 | description: "为一个身分组启用/停用" 116 | content: "**为 %{role} 启用/停用 %{bot}**\n- %{state}\n- %{default_state}" 117 | toggle: 118 | "true": "启用" 119 | "false": "停用" 120 | toggle_all: 121 | "true": "为所有身分组启用" 122 | "false": "为所有身分组停用" 123 | "none": "为所有身分组启用/停用" 124 | toggle_default: 125 | "true": "为新身分组启用" 126 | "false": "为新身分组停用" 127 | premium: "新身分组的更改(会员功能)" 128 | state: 129 | "true": "🟢 为 %{role} 启用" 130 | "false": "🔴 为 %{role} 停用" 131 | all_state: 132 | "true": "🟢 为所有身分组启用" 133 | "false": "🔴 为所有身分组停用" 134 | default_state: 135 | "true": "🟢 为新身分组启用(预设)" 136 | "false": "🔴 为新身分组停用(预设)" 137 | reply_method: 138 | name: "回复方式" 139 | description: "更改回复时的动作" 140 | content: "**更改回复时要做什麽**\n- %{state}\n- %{silent}%{perms}" 141 | reply: 142 | button: 143 | "true": "回复" 144 | "false": "发送" 145 | state: 146 | "true": "%{emoji} 回复原始讯息" 147 | "false": "📨 单纯发送讯息" 148 | silent: 149 | button: 150 | "true": "静音" 151 | "false": "有通知" 152 | state: 153 | "true": "🔕 讯息会透过 @silent 传送" 154 | "false": "🔔 讯息传送时会发送通知" 155 | webhooks: 156 | name: "Webhooks" 157 | description: "为 Webhooks 启用/停用" 158 | content: "**更改 Webhooks 的动作**\n%{state}" 159 | button: 160 | "true": "回复" 161 | "false": "忽略" 162 | state: 163 | "true": "🟢 回复 Webhooks" 164 | "false": "🔴 忽略 Webhooks" 165 | original_message: 166 | name: "原始讯息" 167 | description: "更改对原始讯息的动作" 168 | content: "**更改要对原始讯息做什麽**\n%{state}%{perms}" 169 | option: 170 | nothing: 171 | label: "什麽都不做" 172 | emoji: "🚫" 173 | remove_embeds: 174 | label: "移除嵌入" 175 | emoji: "✂️" 176 | delete: 177 | label: "完全删除讯息" 178 | emoji: "🗑️" 179 | troubleshooting: 180 | name: "疑难排解" 181 | description: "查看机器人的状态以及排解常见问题" 182 | ping: 183 | name: "延迟" 184 | value: "%{latency} 毫秒" 185 | premium: 186 | name: "会员" 187 | "true": "✨ 这个伺服器已启用会员功能!✨" 188 | "false": "这个伺服器没有会员。" 189 | permissions: "在 %{channel} 的权限" 190 | options: "选项" 191 | websites: "网站" 192 | refresh: "重新整理" 193 | custom_websites: "自订网站" 194 | websites: 195 | name: "网站" 196 | description: "更改不同网站的设定" 197 | placeholder: "选择一个网站来编辑" 198 | content: "**更改不同网站的设定**\n\n选择一个网站来编辑它的设定" 199 | base_website: 200 | description: "更改 %{name} 连结的设定" 201 | content: "**启用/停用 %{name} 连结的修復**\n%{state}%{view}\n-# 致谢: %{credits}" 202 | view: 203 | normal: 204 | label: "一般显示" 205 | emoji: "🔗" 206 | gallery: 207 | label: "相簿显示" 208 | emoji: "🖼️" 209 | text_only: 210 | label: "仅限文字显示" 211 | emoji: "📝" 212 | direct_media: 213 | label: "直接媒体显示" 214 | emoji: "📸" 215 | state: 216 | "true": "🟢 修復 %{name} 连结" 217 | "false": "🔴 不修復 %{name} 连结" 218 | button: 219 | "true": "启用" 220 | "false": "停用" 221 | twitter: 222 | name: "Twitter" 223 | description: "更改 Twitter 连结的设定" 224 | content: "**启用/停用 Twitter 连结的修復及管理翻译**\n%{state}\n%{view}\n-# 致谢: %{credits}" 225 | state: 226 | "true": "🟢 修復 Twitter 连结" 227 | "false": "🔴 不修復 Twitter 连结" 228 | translation: 229 | "true": " 并翻译它们到 '%{lang}'" 230 | "false": " 但不翻译" 231 | button: 232 | state: 233 | "true": "启用" 234 | "false": "停用" 235 | translation: 236 | "true": "翻译到 \"%{lang}\"" 237 | "false": "停用翻译" 238 | translation_lang: "编辑翻译语言" 239 | modal: 240 | title: "编辑翻译语言" 241 | label: "翻译语言" 242 | placeholder: "输入翻译的 2 字母 ISO 语言代码(例:\"cn\")" 243 | error: "\"%{lang}\" 不是一个有效的语言。请输入一个有效的 2 字母 ISO 语言代码(例:\"cn\")。[ISO 语言代码列表]()" 244 | custom_websites: 245 | name: "自订网站" 246 | description: "新增或移除自订网站的修復" 247 | content: "**新增或移除自订网站的修復**" 248 | list: "\n\n已註册的网站:\n" 249 | website: "- %{name}: `%{domain}` → `%{fix_domain}`" 250 | selected_website: "- **%{name}: `%{domain}` → `%{fix_domain}`**" 251 | placeholder: "选择一个网站来编辑" 252 | empty: "没有已註册的网站" 253 | button: 254 | add: "新增网站" 255 | edit: "编辑网站" 256 | delete: "移除网站" 257 | premium: "新增网站(会员可多于 3 个网站)" 258 | max: "新增网站(无法新增多于 25 个网站)" 259 | modal: 260 | title: "新增一个自订网站" 261 | name: 262 | label: "网站名称" 263 | placeholder: "输入网站的名称(例:\"我的网站\")" 264 | domain: 265 | label: "网站域名" 266 | placeholder: "输入网站的域名(例:\"mywebsite.com\")" 267 | fix_domain: 268 | label: "修復的网域" 269 | placeholder: "输入修復的网域(例:\"fxmywebsite.com\")" 270 | error: 271 | exists: "这个网站已经有存在的修復了" 272 | length_name: "网站名称的长度必须少于 %{max} 字元" 273 | length_domain: "网站域名的长度必须少于 %{max} 字元" 274 | misc: 275 | discord_discovery_description: | 276 | FixTweetBot 是一个 Discord 机器人,它使用线上服务(例如 [FxTwitter](https://github.com/FixTweet/FxTwitter))来修復社群媒体嵌入。 277 | 278 | **具体来说,这个机器人会自动将社群媒体连结重新发佈为包含更好嵌入的「修復」版本(例如:允许直接在 Discord 中播放影片)。 ** 279 | 280 | ## 功能与亮点 281 | 282 | - 支援 Twitter、Nitter、Instagram、TikTok、Reddit、Threads、Bluesky、Snapchat、Facebook、Pixiv、Twitch、Spotify、DeviantArt、Mastodon、Tumblr、BiliBili、IFunny、Fur Affinity、YouTube 以及您设定的任何自订网站 283 | - 推文翻译 284 | - 停用个别的网站、频道、成员或身分组 285 | - 可自由自订的动作和外观 286 | - 支援多种语言 287 | - 现代化设定介面 288 | - 可以回应机器人和 webhook 289 | - 遵循 Markdown 语法 290 | - 尊重隐私 291 | - 可检视源码 292 | 293 | ## 用法 294 | 295 | 只需发送包含支援的社群媒体连结的讯息,机器人就会删除嵌入的内容(如果有),并自动将其重新发佈为「修復的」连结。 296 | 297 | 你也可以将连结放在 `<` 和 `>` 之间来忽略它,如下所示:``。 298 | 299 | 你可以使用 `/设定` 指令管理机器人的设定。 300 | 301 | 最后,你可以随时使用 `/关于` 指令了解有关机器人的更多资讯并取得协助。 302 | -------------------------------------------------------------------------------- /locales/zh-TW.yml: -------------------------------------------------------------------------------- 1 | placeholder: 0 2 | about: 3 | command: 4 | name: "關於" 5 | description: "取得關於機器人的資訊" 6 | name: "關於" 7 | description: "這個機器人會自動將社群媒體連結重新發佈為「修復的」版本。" 8 | help: 9 | name: "幫助" 10 | value: "使用 `/設定` 在您的群組中設定機器人。如果您在使用機器人時遇到問題,請使用 `/設定` 命令的 `故障排除` 部分。加入 [支援群組](https://discord.gg/3ej9JrkF3U) 以取得更多協助。" 11 | premium: 12 | name: "會員" 13 | "true": "✨ 這個群組已啟用會員功能! ✨" 14 | "false": "這個群組沒有會員。" 15 | invite: "邀請" 16 | source: "原始碼" 17 | support: "支援群組" 18 | links: 19 | name: "連結" 20 | value: | 21 | - [邀請連結](%{invite_link}) 22 | - [Tog.gg 頁面](https://top.gg/bot/1164651057243238400)(請投票!) 23 | - [原始碼](%{repo_link})(請留下星星!) 24 | - [翻譯專案](https://crowdin.com/project/fixtweetbot)(協助我們翻譯成你的語言!) 25 | - [代理/修復程式積分](https://github.com/Kyrela/FixTweetBot?tab=readme-ov-file#proxies) 26 | - [支援群組](%{support_link}) 27 | settings: 28 | command: 29 | name: "設定" 30 | description: "管理 FixTweet 設定" 31 | channel: 32 | name: "頻道" 33 | description: "管理 FixTweet 設定的頻道" 34 | member: 35 | name: "成員" 36 | description: "負責管理 FixTweet 設定的成員" 37 | role: 38 | name: "身分組" 39 | description: "管理 FixTweet 設定的身分組" 40 | title: "設定" 41 | description: "選擇一個設定以查看其詳細資訊" 42 | placeholder: "選擇一個設定" 43 | perms: 44 | scope: " 在 %{scope}" 45 | label: "\n\n在 %{channel} 的權限:\n" 46 | missing_label: "\n\n**缺少的權限:**\n" 47 | view_channel: 48 | "true": "🟢 `檢視頻道` 權限" 49 | "false": "🔴 缺少 `檢視頻道` 權限" 50 | send_messages: 51 | "true": "🟢 `發送訊息` 權限" 52 | "false": "🔴 缺少 `發送訊息` 權限" 53 | send_messages_in_threads: 54 | "true": "🟢 `在討論串中傳送訊息` 權限" 55 | "false": "🔴 缺少 `在討論串中傳送訊息` 權限" 56 | embed_links: 57 | "true": "🟢 `嵌入連結` 權限" 58 | "false": "🔴 缺少 `嵌入連結` 權限" 59 | manage_messages: 60 | "true": "🟢 `管理訊息` 權限" 61 | "false": "🔴 缺少 `管理訊息` 權限" 62 | read_message_history: 63 | "true": "🟢 `讀取訊息歷史` 權限" 64 | "false": "🔴 缺少 `讀取訊息歷史` 權限" 65 | channel: 66 | name: "頻道" 67 | description: "在一個頻道上啟用/停用" 68 | content: "**在 %{channel} 內啟用/停用 %{bot}**\n- %{state}\n- %{default_state}%{perms}" 69 | toggle: 70 | "true": "啟用" 71 | "false": "停用" 72 | toggle_all: 73 | "true": "在所有地方啟用" 74 | "false": "在所有地方停用" 75 | "none": "在所有地方啟用/停用" 76 | toggle_default: 77 | "true": "為新頻道啟用" 78 | "false": "為新頻道停用" 79 | "premium": "新頻道的更改(會員功能)" 80 | state: 81 | "true": "🟢 在 %{channel} 啟用" 82 | "false": "🔴 在 %{channel} 停用" 83 | all_state: 84 | "true": "🟢 在所有頻道啟用" 85 | "false": "🔴 在所有頻道停用" 86 | default_state: 87 | "true": "🟢 為新頻道啟用(預設)" 88 | "false": "🔴 為新頻道停用(預設)" 89 | member: 90 | name: "成員" 91 | description: "為一個成員啟用/停用" 92 | content: "**為 %{member} 啟用/停用 %{bot}**\n- %{state}\n- %{default_state}" 93 | toggle: 94 | "true": "啟用" 95 | "false": "停用" 96 | toggle_all: 97 | "true": "為所有人啟用" 98 | "false": "為所有人停用" 99 | "none": "為所有人啟用/停用" 100 | toggle_default: 101 | "true": "為新成員啟用" 102 | "false": "為新成員停用" 103 | premium: "新成員的更改(會員功能)" 104 | state: 105 | "true": "🟢 為 %{member} 啟用" 106 | "false": "🔴 為 %{member} 停用" 107 | all_state: 108 | "true": "🟢 為所有人啟用" 109 | "false": "🔴 為所有人停用" 110 | default_state: 111 | "true": "🟢 為新成員啟用(預設)" 112 | "false": "🔴 為新成員停用(預設)" 113 | role: 114 | name: "身分組" 115 | description: "為一個身分組啟用/停用" 116 | content: "**為 %{role} 啟用/停用 %{bot}**\n- %{state}\n- %{default_state}" 117 | toggle: 118 | "true": "啟用" 119 | "false": "停用" 120 | toggle_all: 121 | "true": "為所有身分組啟用" 122 | "false": "為所有身分組停用" 123 | "none": "為所有身分組啟用/停用" 124 | toggle_default: 125 | "true": "為新身分組啟用" 126 | "false": "為新身分組停用" 127 | premium: "新身分組的更改(會員功能)" 128 | state: 129 | "true": "🟢 為 %{role} 啟用" 130 | "false": "🔴 為 %{role} 停用" 131 | all_state: 132 | "true": "🟢 為所有身分組啟用" 133 | "false": "🔴 為所有身分組停用" 134 | default_state: 135 | "true": "🟢 為新身分組啟用(預設)" 136 | "false": "🔴 為新身分組停用(預設)" 137 | reply_method: 138 | name: "回覆方式" 139 | description: "更改回覆時的動作" 140 | content: "**更改回覆時要做什麼**\n- %{state}\n- %{silent}%{perms}" 141 | reply: 142 | button: 143 | "true": "回覆" 144 | "false": "發送" 145 | state: 146 | "true": "%{emoji} 回覆原始訊息" 147 | "false": "📨 單純發送訊息" 148 | silent: 149 | button: 150 | "true": "靜音" 151 | "false": "有通知" 152 | state: 153 | "true": "🔕 訊息會透過 @silent 傳送" 154 | "false": "🔔 訊息傳送時會發送通知" 155 | webhooks: 156 | name: "Webhooks" 157 | description: "為 Webhooks 啟用/停用" 158 | content: "**更改 Webhooks 的動作**\n%{state}" 159 | button: 160 | "true": "回覆" 161 | "false": "忽略" 162 | state: 163 | "true": "🟢 回覆 Webhooks" 164 | "false": "🔴 忽略 Webhooks" 165 | original_message: 166 | name: "原始訊息" 167 | description: "更改對原始訊息的動作" 168 | content: "**更改要對原始訊息做什麼**\n%{state}%{perms}" 169 | option: 170 | nothing: 171 | label: "什麼都不做" 172 | emoji: "🚫" 173 | remove_embeds: 174 | label: "移除嵌入" 175 | emoji: "✂️" 176 | delete: 177 | label: "完全刪除訊息" 178 | emoji: "🗑️" 179 | troubleshooting: 180 | name: "疑難排解" 181 | description: "查看機器人的狀態以及排解常見問題" 182 | ping: 183 | name: "延遲" 184 | value: "%{latency} 毫秒" 185 | premium: 186 | name: "會員" 187 | "true": "✨ 這個伺服器已啟用會員功能!✨" 188 | "false": "這個伺服器沒有會員。" 189 | permissions: "在 %{channel} 的權限" 190 | options: "選項" 191 | websites: "網站" 192 | refresh: "重新整理" 193 | custom_websites: "自訂網站" 194 | websites: 195 | name: "網站" 196 | description: "更改不同網站的設定" 197 | placeholder: "選擇一個網站來編輯" 198 | content: "**更改不同網站的設定**\n\n選擇一個網站來編輯它的設定" 199 | base_website: 200 | description: "更改 %{name} 連結的設定" 201 | content: "**啟用/停用 %{name} 連結的修復**\n%{state}%{view}\n-# 致謝: %{credits}" 202 | view: 203 | normal: 204 | label: "一般顯示" 205 | emoji: "🔗" 206 | gallery: 207 | label: "相簿顯示" 208 | emoji: "🖼️" 209 | text_only: 210 | label: "僅限文字顯示" 211 | emoji: "📝" 212 | direct_media: 213 | label: "直接媒體顯示" 214 | emoji: "📸" 215 | state: 216 | "true": "🟢 修復 %{name} 連結" 217 | "false": "🔴 不修復 %{name} 連結" 218 | button: 219 | "true": "啟用" 220 | "false": "停用" 221 | twitter: 222 | name: "Twitter" 223 | description: "更改 Twitter 連結的設定" 224 | content: "**啟用/停用 Twitter 連結的修復及管理翻譯**\n%{state}\n%{view}\n-# 致謝: %{credits}" 225 | state: 226 | "true": "🟢 修復 Twitter 連結" 227 | "false": "🔴 不修復 Twitter 連結" 228 | translation: 229 | "true": " 並翻譯它們到 '%{lang}'" 230 | "false": " 但不翻譯" 231 | button: 232 | state: 233 | "true": "啟用" 234 | "false": "停用" 235 | translation: 236 | "true": "翻譯到 \"%{lang}\"" 237 | "false": "停用翻譯" 238 | translation_lang: "編輯翻譯語言" 239 | modal: 240 | title: "編輯翻譯語言" 241 | label: "翻譯語言" 242 | placeholder: "輸入翻譯的 2 字母 ISO 語言代碼(例:\"tw\")" 243 | error: "\"%{lang}\" 不是一個有效的語言。請輸入一個有效的 2 字母 ISO 語言代碼(例:\"tw\")。[ISO 語言代碼列表]()" 244 | custom_websites: 245 | name: "自訂網站" 246 | description: "新增或移除自訂網站的修復" 247 | content: "**新增或移除自訂網站的修復**" 248 | list: "\n\n已註冊的網站:\n" 249 | website: "- %{name}: `%{domain}` → `%{fix_domain}`" 250 | selected_website: "- **%{name}: `%{domain}` → `%{fix_domain}`**" 251 | placeholder: "選擇一個網站來編輯" 252 | empty: "沒有已註冊的網站" 253 | button: 254 | add: "新增網站" 255 | edit: "編輯網站" 256 | delete: "移除網站" 257 | premium: "新增網站(會員可多於 3 個網站)" 258 | max: "新增網站(無法新增多於 25 個網站)" 259 | modal: 260 | title: "新增一個自訂網站" 261 | name: 262 | label: "網站名稱" 263 | placeholder: "輸入網站的名稱(例:\"我的網站\")" 264 | domain: 265 | label: "網站域名" 266 | placeholder: "輸入網站的域名(例:\"mywebsite.com\")" 267 | fix_domain: 268 | label: "修復的網域" 269 | placeholder: "輸入修復的網域(例:\"fxmywebsite.com\")" 270 | error: 271 | exists: "這個網站已經有存在的修復了" 272 | length_name: "網站名稱的長度必須少於 %{max} 字元" 273 | length_domain: "網站域名的長度必須少於 %{max} 字元" 274 | misc: 275 | discord_discovery_description: | 276 | FixTweetBot 是一個 Discord 機器人,它使用線上服務(例如 [FxTwitter](https://github.com/FixTweet/FxTwitter))來修復社群媒體嵌入。 277 | 278 | **具體來說,這個機器人會自動將社群媒體連結重新發佈為包含更好嵌入的「修復」版本(例如:允許直接在 Discord 中播放影片)。 ** 279 | 280 | ## 功能與亮點 281 | 282 | - 支援 Twitter、Nitter、Instagram、TikTok、Reddit、Threads、Bluesky、Snapchat、Facebook、Pixiv、Twitch、Spotify、DeviantArt、Mastodon、Tumblr、BiliBili、IFunny、Fur Affinity、YouTube 以及您設定的任何自訂網站 283 | - 推文翻譯 284 | - 停用個別的網站、頻道、成員或身分組 285 | - 可自由自訂的動作和外觀 286 | - 支援多種語言 287 | - 現代化設定介面 288 | - 可以回應機器人和 webhook 289 | - 遵循 Markdown 語法 290 | - 尊重隱私 291 | - 可檢視源碼 292 | 293 | ## 用法 294 | 295 | 只需發送包含支援的社群媒體連結的訊息,機器人就會刪除嵌入的內容(如果有),並自動將其重新發佈為「修復的」連結。 296 | 297 | 你也可以將連結放在 `<` 和 `>` 之間來忽略它,如下所示:``。 298 | 299 | 你可以使用 `/設定` 指令管理機器人的設定。 300 | 301 | 最後,你可以隨時使用 `/關於` 指令了解有關機器人的更多資訊並取得協助。 302 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import discore 4 | 5 | from src.utils import I18nTranslator 6 | 7 | os.environ['DB_CONFIG_PATH'] = 'database/config.py' 8 | 9 | intents = discore.Intents(guild_messages=True, message_content=True, guilds=True, members=True) 10 | 11 | bot = discore.Bot(help_command=None, intents=intents) 12 | asyncio.run(bot.tree.set_translator(I18nTranslator())) 13 | bot.run() 14 | -------------------------------------------------------------------------------- /privacy-policy.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy for FixTweetBot 2 | **Effective Date:** 06/05/2025 3 | 4 | This Privacy Policy explains how *FixTweetBot* (“the Bot”, “we”, “our”) collects, uses, stores, and manages data in connection with its operation as a Discord bot. By using *FixTweetBot*, you acknowledge that you have read and understood this policy. 5 | 6 | --- 7 | 8 | ## 1. Data Controller 9 | The Bot is operated by an individual under the pseudonym **Kyrela**, based in **France**. The service is provided globally and is not restricted to any specific region, though it operates exclusively on the Discord platform. 10 | 11 | --- 12 | 13 | ## 2. Data Collected 14 | 15 | FixTweetBot collects the following categories of data: 16 | 17 | - **User-specific Discord data:** User IDs, role IDs, server (guild) IDs, and text channel IDs. 18 | - **Service configuration data:** Custom settings saved by users for link fixing behavior. 19 | - **Operational logs (limited):** 20 | - Logs of errors including the user ID, guild ID, and any information that could help resolve the error, depending on the context. 21 | - Logs of successful usage events, including event type and timestamp. 22 | 23 | **Note:** The bot does *not* collect or store message content or metadata beyond the above logging. 24 | 25 | --- 26 | 27 | ## 3. Scope of Data Collection 28 | 29 | - The Bot does not operate or collect data via direct messages (DMs). 30 | - Data may be collected within private servers if the bot is explicitly added there by a server administrator. 31 | 32 | --- 33 | 34 | ## 4. Purpose of Data Use 35 | 36 | - **Command Functionality:** Data such as IDs and settings are used solely to execute the bot's link-fixing features, including per-server configuration. 37 | - **Event Logging:** Event data is collected for non-identifying analytics, such as usage trends or performance display. 38 | - **Error Tracking:** Error logs help identify and resolve bugs. 39 | 40 | No profiling, advertising, or automated decision-making is performed using the data. 41 | 42 | --- 43 | 44 | ## 5. Data Storage and Security 45 | 46 | - Data is stored on a privately operated Linux server located and maintained by the operator. 47 | - Server access is restricted and protected using standard Linux security practices, with account-based access control and regular updates to address vulnerabilities. 48 | - Logs are purged at each software update; service data remains persistent unless deleted at the user’s request. 49 | 50 | Details of server architecture are intentionally withheld to protect security integrity. 51 | 52 | --- 53 | 54 | ## 6. Data Sharing and Third Parties 55 | 56 | - No user data is shared with third parties. 57 | - The Bot does not rely on or transmit data to any external APIs or third-party services that would receive user information. 58 | 59 | --- 60 | 61 | ## 7. User Rights 62 | 63 | Users may at any time: 64 | 65 | - Request a copy of the data FixTweetBot holds about them. 66 | - Request deletion of their stored data. 67 | 68 | Requests can be submitted via: 69 | 70 | - The official GitHub repository: [FixTweetBot GitHub](https://github.com/Kyrela/FixTweetBot) 71 | - The official Discord support server: [FixTweetBot Discord](https://discord.gg/3ej9JrkF3U) 72 | - Email (as provided in bot documentation or GitHub profile) 73 | 74 | --- 75 | 76 | ## 8. Children’s Privacy 77 | 78 | This Bot is not directed toward individuals under the age of 13. As per Discord’s own Terms of Service, users must be 13 or older (or the minimum age of digital consent in their jurisdiction) to use the platform and, by extension, this Bot. 79 | 80 | --- 81 | 82 | ## 9. Policy Updates 83 | 84 | This Privacy Policy may be updated from time to time. Users can review the most recent version at: 85 | **[https://github.com/Kyrela/FixTweetBot/blob/main/privacy-policy.md](https://github.com/Kyrela/FixTweetBot/blob/main/privacy-policy.md)** 86 | The document includes the date of the last update at the top of the page. 87 | 88 | --- 89 | 90 | ## Contact 91 | 92 | For any privacy-related questions or concerns, please contact the operator via the support platforms listed above. 93 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | git+https://github.com/Kyrela/discore 2 | python-i18n~=0.3.9 3 | psutil~=6.1.1 4 | requests~=2.32.0 5 | masonite-orm~=2.24.0 6 | pymysql~=1.1.1 7 | git+https://github.com/Kyrela/discord-markdown-ast-parser 8 | git+https://github.com/null8626/python-sdk@patch-1 9 | aiohttp~=3.11.14 10 | -------------------------------------------------------------------------------- /src/utils.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from typing import TypeVar, Any, Optional, Iterable 3 | 4 | from discord.app_commands import locale_str 5 | from i18n import * 6 | import i18n 7 | from i18n.translator import TranslationFormatter, pluralize 8 | import discore 9 | 10 | 11 | def t(key, **kwargs): 12 | """ 13 | Translate a key with security 14 | 15 | :param key: The key to translate 16 | :param kwargs: The arguments to pass to the translation 17 | :return: The translated key 18 | """ 19 | 20 | locale = kwargs.pop('locale', config.get('locale')) 21 | if translations.has(key, locale): 22 | return translate(key, locale=locale, **kwargs) 23 | else: 24 | resource_loader.search_translation(key, locale) 25 | if translations.has(key, locale): 26 | return translate(key, locale=locale, **kwargs) 27 | elif 'default' in kwargs: 28 | return kwargs['default'] 29 | elif locale != config.get('fallback'): 30 | return t(key, locale=config.get('fallback'), **kwargs) 31 | if config.get('error_on_missing_translation'): 32 | raise KeyError('key {0} not found'.format(key)) 33 | else: 34 | return key 35 | 36 | 37 | def translate(key, **kwargs): 38 | """ 39 | Translate a key 40 | 41 | :param key: The key to translate 42 | :param kwargs: The arguments to pass to the translation 43 | :return: The translated key 44 | """ 45 | 46 | locale = kwargs.pop('locale', config.get('locale')) 47 | translation = translations.get(key, locale=locale) 48 | if 'count' in kwargs: 49 | translation = pluralize(key, translation, kwargs['count']) 50 | return object_format(translation, **kwargs) 51 | 52 | 53 | def object_format(object, **kwargs): 54 | """ 55 | Format a template 56 | 57 | :param object: The object to format 58 | :param kwargs: The arguments to pass to the template 59 | :return: The formatted object 60 | """ 61 | 62 | if isinstance(object, str): 63 | return TranslationFormatter(object).format(**kwargs) 64 | if isinstance(object, list): 65 | return [object_format(elem, **kwargs) for elem in object] 66 | if isinstance(object, dict): 67 | return {key: object_format(value, **kwargs) for key, value in object.items()} 68 | return object 69 | 70 | 71 | V = TypeVar('V', bound='discore.ui.View', covariant=True) 72 | I = TypeVar('I', bound='discore.ui.Item[discore.ui.View]') 73 | 74 | 75 | def edit_callback(item: I, view: V, callback: discore.ui.item.ItemCallbackType[Any, Any]) -> I: 76 | """ 77 | Edit the callback of an item 78 | :param item: The item to add the callback to 79 | :param view: The view in which the item is 80 | :param callback: The callback to add 81 | :return: The item 82 | """ 83 | if not inspect.iscoroutinefunction(callback): 84 | raise TypeError('item callback must be a coroutine function') 85 | 86 | item.callback = discore.ui.view._ViewCallback(callback, view, item) 87 | setattr(view, callback.__name__, item) 88 | return item 89 | 90 | 91 | def is_premium(i: discore.Interaction) -> Optional[bool]: 92 | """ 93 | Check if the user is premium 94 | :param i: The interaction 95 | :return: True if the user is premium, False otherwise. None if no sku is registered 96 | """ 97 | if not is_sku(): 98 | return None 99 | if discore.config.sku is True: 100 | return True 101 | entitlement = next(( 102 | entitlement for entitlement in i.entitlements 103 | if entitlement.sku_id == discore.config.sku), None) 104 | if entitlement is None: 105 | return False 106 | return not entitlement.is_expired() 107 | 108 | 109 | def is_sku() -> bool: 110 | """ 111 | Check if the bot has a sku 112 | :return: True if the bot has a sku, False otherwise 113 | """ 114 | return bool(discore.config.sku) 115 | 116 | 117 | def format_perms( 118 | perms: Iterable[str], 119 | channel: discore.TextChannel | discore.Thread, 120 | include_label: bool = True, 121 | include_valid: bool = False 122 | ) -> str: 123 | """ 124 | Check for permissions in channel and format them into a human readable string 125 | 126 | :param perms: The permissions to check for 127 | :param channel: The channel to check the permissions in 128 | :param include_label: Whether to include the 'permission' label in the formatted permissions 129 | :param include_valid: Whether to include the permissions that are already given to the bot (valid) 130 | :return: The formatted permissions 131 | """ 132 | 133 | if not perms: 134 | return '' 135 | channel_permissions = channel.permissions_for(channel.guild.me) 136 | guild_permissions = channel.guild.me.guild_permissions 137 | str_perms = "\n".join([ 138 | '- ' + t(f'settings.perms.{perm}.{str(perm_value := getattr(channel_permissions, perm)).lower()}') 139 | + ('' if perm_value else t( 140 | 'settings.perms.scope', scope=( 141 | channel.mention 142 | if getattr(guild_permissions, perm) 143 | else f"`{discore.utils.escape_markdown(channel.guild.name, as_needed=True)}`") 144 | )) 145 | for perm in perms 146 | if include_valid or not getattr(channel_permissions, perm) 147 | ]) 148 | if include_label and str_perms: 149 | return t( 150 | 'settings.perms.label' if include_valid else 'settings.perms.missing_label', 151 | channel=channel.mention) + str_perms 152 | return str_perms 153 | 154 | def is_missing_perm(perms: Iterable[str], channel: discore.TextChannel | discore.Thread) -> bool: 155 | """ 156 | Check for missing permissions in channel 157 | 158 | :param perms: The permissions to check for 159 | :param channel: The channel to check the permissions in 160 | :return: True if a permission is missing, False otherwise 161 | """ 162 | 163 | if not perms: 164 | return False 165 | channel_permissions = channel.permissions_for(channel.guild.me) 166 | return any(not getattr(channel_permissions, perm) for perm in perms) 167 | 168 | 169 | class I18nTranslator(discore.app_commands.Translator): 170 | """ 171 | A translator that uses the i18n module 172 | """ 173 | 174 | async def translate(self, locale_str: discore.app_commands.locale_str, locale: discore.Locale, _): 175 | # noinspection PyUnresolvedReferences 176 | return t(locale=locale.value, default=None, **locale_str.extras) 177 | 178 | 179 | def tstr(key: str, **kwargs) -> locale_str: 180 | """ 181 | Generate a locale_str with default message 182 | 183 | :param key: The key to translate 184 | :param kwargs: The arguments to pass to the translation 185 | :return: The locale_str 186 | """ 187 | 188 | return locale_str(t(key, locale=i18n.config.get('fallback'), **kwargs), key=key, **kwargs) 189 | 190 | def group_join(l: list[str], max_group_size: int, sep: str = "\n") -> list[str]: 191 | """ 192 | Group items from a list into strings based on a maximum group size and a separator. 193 | 194 | This function takes a list of strings and combines them into groups of strings, where each group 195 | contains up to a specified maximum number of characters. Groups are formed by concatenating items 196 | from the list with a specified separator. This is useful for formatting output or preparing 197 | blocks of text with size constraints. 198 | 199 | :param l: The list of strings to group. 200 | :param max_group_size: The maximum allowed size (in characters) for each group. 201 | :param sep: The separator to use between items in a group. Default is a newline character. 202 | :return: A list of grouped strings, each string formed by concatenating items from the input 203 | list while adhering to the maximum group size constraint. 204 | """ 205 | 206 | groups = [] 207 | for item in l: 208 | if not groups: 209 | groups.append(item) 210 | elif len(groups[-1]) + len(sep) + len(item) <= max_group_size: 211 | groups[-1] += sep + item 212 | else: 213 | groups.append(item) 214 | 215 | return groups 216 | -------------------------------------------------------------------------------- /src/websites.py: -------------------------------------------------------------------------------- 1 | """ 2 | Allows fixing links from various websites. 3 | """ 4 | import asyncio 5 | import re 6 | from typing import Optional, Self, Type, Iterable, Callable 7 | 8 | import aiohttp 9 | 10 | from database.models.Guild import * 11 | 12 | __all__ = ('WebsiteLink', 'websites') 13 | 14 | def call_if_valid(func: Callable) -> Callable: 15 | """ 16 | A static method decorator that ensures the wrapped function is executed only if 17 | the instance is in a valid state. If the instance is not valid, a `ValueError` 18 | is raised. 19 | 20 | :param func: The function to be wrapped and whose execution is conditional 21 | upon the validity of the instance. 22 | :return: A wrapper function that enforces a validity check before executing 23 | the wrapped function. 24 | """ 25 | def wrapper(self, *args, **kwargs): 26 | """ 27 | Represents a link to a website and provides functionality to validate it 28 | before executing specific operations. The static method `call_if_valid` 29 | is used as a decorator to ensure that any decorated method is only executed 30 | if the calling instance is valid. 31 | """ 32 | if self.is_valid(): 33 | return func(self, *args, **kwargs) 34 | raise ValueError("Invalid website link") 35 | return wrapper 36 | 37 | class WebsiteLink: 38 | """ 39 | Base class for all websites. 40 | """ 41 | 42 | name: str 43 | id: str 44 | 45 | def __init__(self, guild: Guild, url: str) -> None: 46 | """ 47 | Initialize the website. 48 | 49 | :param guild: The guild where the link has been sent 50 | :param url: The URL to fix 51 | """ 52 | 53 | self.guild: Guild = guild 54 | self.url: str = url 55 | 56 | @classmethod 57 | def if_valid(cls, *args, **kwargs) -> Optional[Self]: 58 | """ 59 | Return a website if the URL is valid. 60 | 61 | :param args: The arguments to pass to the constructor 62 | :param kwargs: The keyword arguments to pass to the constructor 63 | :return: The website if the URL is valid, None otherwise 64 | """ 65 | 66 | self = cls(*args, **kwargs) 67 | return self if self.is_valid() else None 68 | 69 | def is_valid(self) -> bool: 70 | """ 71 | Indicates if the link is valid. 72 | 73 | :return: True if the link is valid, False otherwise 74 | """ 75 | 76 | raise NotImplementedError 77 | 78 | @call_if_valid 79 | async def get_fixed_url(self) -> tuple[Optional[str], Optional[str]]: 80 | """ 81 | Get the fixed link and its hypertext label. 82 | :return: The fixed link and its hypertext label 83 | """ 84 | raise NotImplementedError 85 | 86 | @call_if_valid 87 | async def get_author_url(self) -> tuple[Optional[str], Optional[str]]: 88 | """ 89 | Get the author link and its hypertext label. 90 | :return: The author link and its hypertext label 91 | """ 92 | raise NotImplementedError 93 | 94 | @call_if_valid 95 | async def get_original_url(self) -> tuple[Optional[str], Optional[str]]: 96 | """ 97 | Get the original link and its hypertext label. 98 | :return: The original link and its hypertext label 99 | """ 100 | raise NotImplementedError 101 | 102 | @call_if_valid 103 | async def render(self) -> Optional[str]: 104 | """ 105 | Render the fixed link according to the guild's settings and the context 106 | 107 | :return: The rendered fixed link 108 | """ 109 | 110 | fixed_url, fixed_label = await self.get_fixed_url() 111 | if not fixed_url: 112 | return None 113 | author_url, author_label = await self.get_author_url() 114 | original_url, original_label = await self.get_original_url() 115 | fixed_link = f"[{original_label}](<{original_url}>)" 116 | if author_url: 117 | fixed_link += f" • [{author_label}](<{author_url}>)" 118 | fixed_link += f" • [{fixed_label}]({fixed_url})" 119 | return fixed_link 120 | 121 | 122 | class GenericWebsiteLink(WebsiteLink): 123 | """ 124 | Represents a generic website link. 125 | """ 126 | 127 | name: str 128 | id: str 129 | hypertext_label: str 130 | fixer_name: str 131 | fix_domain: str 132 | subdomains: Optional[dict[str, str]] = None 133 | routes: dict[str, re.Pattern[str]] = {} 134 | 135 | def __init__(self, guild: Guild, url: str) -> None: 136 | """ 137 | Initialize the website. 138 | 139 | :param url: the URL of the website 140 | :param guild: the guild where the link check is happening 141 | :return: None 142 | """ 143 | 144 | super().__init__(guild, url) 145 | self.match, self.repl = self.get_match_and_repl() 146 | 147 | @classmethod 148 | def if_valid(cls, guild: Guild, url: str) -> Optional[Self]: 149 | """ 150 | Return a website if the URL is valid. 151 | 152 | :param guild: the guild where the link check is happening 153 | :param url: the URL to check 154 | :return: the website if the URL is valid, None otherwise 155 | """ 156 | 157 | if not guild.__getattr__(cls.id): 158 | return None 159 | 160 | website = cls(guild, url) 161 | return website if website.is_valid() else None 162 | 163 | def is_valid(self) -> bool: 164 | return True if self.match else False 165 | 166 | def get_repl(self, route: str, match: re.Match[str]) -> str: 167 | """ 168 | Generate a replacement for the corresponding route, with named groups. 169 | :param route: the route to generate the replacement for 170 | :param match: the match for the corresponding route 171 | """ 172 | 173 | if route[0] != '/': 174 | route = '/' + route 175 | 176 | found_path_segments = [match[1] for match in re.finditer(r":(\w+)(?:\([^/]+\))?", route)] 177 | 178 | params = [ 179 | g_name 180 | for g_name, g_value in match.groupdict().items() 181 | if g_name not in found_path_segments and g_value is not None and g_name not in ('domain', 'subdomain') 182 | ] 183 | 184 | route_repl = route 185 | route_repl = re.sub(r"/:(\w+)(?:\([^/]+\))?\?", r"", route_repl) 186 | route_repl = re.sub(r":(\w+)(?:\([^/]+\))?", r"\\g<\1>", route_repl) 187 | 188 | query_string_repl = '' 189 | if params: 190 | query_string_repl = '?' + '&'.join(rf"{param}=\g<{param}>" for param in params) 191 | 192 | return ( 193 | "https://{domain}" 194 | + route_repl 195 | + self.route_fix_post_path_segments() 196 | + query_string_repl 197 | ) 198 | 199 | def route_fix_post_path_segments(self) -> str: 200 | """ 201 | Provide supplementary path segments for the fixed link. 202 | 203 | :return: the supplementary path segments 204 | """ 205 | 206 | return "" 207 | 208 | def get_match_and_repl(self) -> tuple[Optional[re.Match[str]], Optional[str]]: 209 | """ 210 | Get the match for the fixed link, if any, and generate a replacement for the corresponding route. 211 | 212 | :return: the match for the fixed link and the replacement for the corresponding route, or None if no match is found 213 | """ 214 | 215 | for route, regex in self.routes.items(): 216 | if match := regex.fullmatch(self.url): 217 | return match, self.get_repl(route, match) 218 | return None, None 219 | 220 | @call_if_valid 221 | async def get_fixed_url(self) -> tuple[Optional[str], Optional[str]]: 222 | subdomain = '' 223 | if self.subdomains: 224 | subdomain = self.subdomains[self.guild.__getattr__(f"{self.id}_view")] 225 | fixed_url = self.match.expand(self.repl.format(domain=subdomain + self.fix_domain)) 226 | return fixed_url, self.fixer_name 227 | 228 | @call_if_valid 229 | async def get_author_url(self) -> tuple[Optional[str], Optional[str]]: 230 | if not ('username' in self.match.groupdict() and self.match['username']): 231 | return None, None 232 | username = self.match["username"] 233 | user_link = (await self.get_original_url())[0].split(username)[0] + username 234 | return user_link, username 235 | 236 | @call_if_valid 237 | async def get_original_url(self) -> tuple[Optional[str], Optional[str]]: 238 | subdomain = "" 239 | if self.match['subdomain'] and self.match['subdomain'] != 'www': 240 | subdomain = self.match['subdomain'] + '.' 241 | original_url = self.match.expand(self.repl.format(domain=subdomain + self.match['domain'])) 242 | return original_url, self.hypertext_label 243 | 244 | 245 | def generate_regex(domain_names: str|list[str], route: str, params: Optional[list[str]] = None) -> re.Pattern[str]: 246 | """ 247 | Generate a regex for the corresponding route, with named groups. 248 | :param domain_names: the domain name to generate the regex for 249 | :param route: the route to generate the regex for 250 | :param params: the parameters to generate the regex with 251 | :return: the generated regex 252 | """ 253 | 254 | if route[0] != '/': 255 | route = '/' + route 256 | 257 | if isinstance(domain_names, str): 258 | domain_names = [domain_names] 259 | 260 | domain_regex = r"(?P" + '|'.join([re.escape(domain_name) for domain_name in domain_names]) + r")" 261 | 262 | route_regex = route 263 | route_regex = re.sub(r"/:(\w+)\(([^/]+)\)\?", r"(?:/\2)?", route_regex) 264 | route_regex = re.sub(r"/:(\w+)\?", r"(?:/[^/?#]+)?", route_regex) 265 | route_regex = re.sub(r":(\w+)\(([^/]+)\)", r"(?P<\1>\2)", route_regex) 266 | route_regex = re.sub(r":(\w+)", r"(?P<\1>[^/?#]+)", route_regex) 267 | 268 | query_string_param_regexes = [] 269 | if params: 270 | query_string_param_regexes = [rf"(?:(?=(?:\?|.*&){param}=(?P<{param}>[^&#]+)))?" for param in params] 271 | query_string_regex = r"/?(?:" + ''.join(query_string_param_regexes) + r"\?[^#]+)?" 272 | 273 | return re.compile(r"https?://(?:(?P[^.]+)\.)?" + domain_regex + route_regex + query_string_regex + r"(?:#.+)?", re.IGNORECASE) 274 | 275 | 276 | def generate_routes(domain_names: str|list[str], routes: dict[str, Optional[list[str]]]) -> dict[str, re.Pattern[str]]: 277 | """ 278 | Generate regexes for the corresponding routes, with named groups. 279 | :param domain_names: the domain name to generate the regexes for 280 | :param routes: the routes to generate the regexes for 281 | :return: the generated regexes 282 | """ 283 | 284 | return { 285 | route: generate_regex(domain_names, route, params) 286 | for route, params in routes.items() 287 | } 288 | 289 | 290 | class EmbedEZLink(GenericWebsiteLink): 291 | fixer_name = "EmbedEZ" 292 | 293 | async def get_fixed_url(self) -> tuple[Optional[str], Optional[str]]: 294 | try: 295 | async with aiohttp.ClientSession() as session: 296 | async with session.get("https://embedez.com/api/v1/providers/combined", params={'q': self.url}, timeout=aiohttp.ClientTimeout(total=30)) as response: 297 | if response.status != 200: 298 | return None, None 299 | except asyncio.TimeoutError: 300 | return None, None 301 | return await super().get_fixed_url() 302 | 303 | 304 | class TwitterLink(GenericWebsiteLink): 305 | """ 306 | Twitter website. 307 | """ 308 | 309 | name = 'Twitter' 310 | id = 'twitter' 311 | hypertext_label = 'Tweet' 312 | fix_domain = "fxtwitter.com" 313 | fixer_name = "FxTwitter" 314 | subdomains = { 315 | TwitterView.NORMAL: 'm.', 316 | TwitterView.GALLERY: 'g.', 317 | TwitterView.TEXT_ONLY: 't.', 318 | TwitterView.DIRECT_MEDIA: 'd.', 319 | } 320 | routes = generate_routes( 321 | ["twitter.com", "x.com", "nitter.net", "xcancel.com", "nitter.poast.org", "nitter.privacyredirect.com", "lightbrd.com", "nitter.space", "nitter.tiekoetter.com"], 322 | { 323 | "/:username/status/:id": None, 324 | "/:username/status/:id/:media_type(photo|video)/:media_id": None, 325 | }) 326 | 327 | def route_fix_post_path_segments(self) -> str: 328 | return f"/{self.guild.twitter_tr_lang}" if self.guild.twitter_tr else "" 329 | 330 | 331 | class InstagramLink(EmbedEZLink): 332 | """ 333 | Instagram website. 334 | """ 335 | 336 | name = 'Instagram' 337 | id = 'instagram' 338 | hypertext_label = 'Instagram' 339 | fix_domain = "instagramez.com" 340 | routes = generate_routes( 341 | "instagram.com", 342 | { 343 | "/:media_type(p|reels?|tv|share)/:id": ['img_index'], 344 | "/:username/:media_type(p|reels?|tv|share)/:id": ['img_index'], 345 | }) 346 | 347 | 348 | class TikTokLink(GenericWebsiteLink): 349 | """ 350 | Tiktok website. 351 | """ 352 | 353 | name = 'TikTok' 354 | id = 'tiktok' 355 | hypertext_label = 'Tiktok' 356 | fix_domain = "tnktok.com" 357 | fixer_name = "fxTikTok" 358 | subdomains = { 359 | TiktokView.NORMAL: 'a.', 360 | TiktokView.GALLERY: '', 361 | TiktokView.DIRECT_MEDIA: 'd.', 362 | } 363 | routes = generate_routes( 364 | "tiktok.com", 365 | { 366 | "/@:username/:media_type(video|photo)/:id": None, 367 | "/:shortlink_type(t|embed)/:id": None, 368 | "/:id": None, 369 | }) 370 | 371 | 372 | class RedditLink(GenericWebsiteLink): 373 | """ 374 | Reddit website. 375 | """ 376 | 377 | name = 'Reddit' 378 | id = 'reddit' 379 | hypertext_label = 'Reddit' 380 | fix_domain = "vxreddit.com" 381 | fixer_name = "vxreddit" 382 | routes = generate_routes( 383 | ["reddit.com", "redditmedia.com"], 384 | { 385 | "/:post_type(u|r|user)/:username/:type(comments|s)/:id/:slug?": None, 386 | "/:post_type(u|r|user)/:username/:type(comments|s)/:id/:slug/:comment": None, 387 | "/:id": None, 388 | }) 389 | 390 | 391 | class ThreadsLink(GenericWebsiteLink): 392 | """ 393 | Threads website. 394 | """ 395 | 396 | name = 'Threads' 397 | id = 'threads' 398 | hypertext_label = 'Threads' 399 | fix_domain = "fixthreads.net" 400 | fixer_name = "FixThreads" 401 | routes = generate_routes( 402 | ["threads.net", "threads.com"], 403 | { 404 | "/@:username/post/:id": None, 405 | }) 406 | 407 | 408 | class BlueskyLink(GenericWebsiteLink): 409 | """ 410 | Bluesky website. 411 | """ 412 | 413 | name = 'Bluesky' 414 | id = 'bluesky' 415 | hypertext_label = 'Bluesky' 416 | fix_domain = "bskx.app" 417 | fixer_name = "VixBluesky" 418 | subdomains = { 419 | BlueskyView.NORMAL: '', 420 | BlueskyView.DIRECT_MEDIA: 'r.', 421 | BlueskyView.GALLERY: 'g.', 422 | } 423 | routes = generate_routes( 424 | "bsky.app", 425 | { 426 | "/profile/did:user_id/post/:id": None, 427 | "/profile/:username/post/:id": None, 428 | }) 429 | 430 | 431 | class SnapchatLink(EmbedEZLink): 432 | """ 433 | Snapchat website. 434 | """ 435 | 436 | name = 'Snapchat' 437 | id = 'snapchat' 438 | hypertext_label = 'Snapchat' 439 | fix_domain = "snapchatez.com" 440 | routes = generate_routes( 441 | "snapchat.com", 442 | { 443 | "/p/:id1/:id2/:id3?": None, 444 | "/spotlight/:id": None, 445 | }) 446 | 447 | 448 | class FacebookLink(EmbedEZLink): 449 | """ 450 | Facebook website. 451 | """ 452 | 453 | name = 'Facebook' 454 | id = 'facebook' 455 | hypertext_label = 'Facebook' 456 | fix_domain = "facebookez.com" 457 | routes = generate_routes( 458 | "facebook.com", 459 | { 460 | "/:username/:media_type(posts|videos)/:id": None, 461 | "/marketplace/item/:marketplace_id": None, 462 | "/share/r/:share_r_id": None, 463 | "/:link_type(share|reel)/:id": None, 464 | "/photos": ['fbid'], 465 | "/photo": ['fbid'], 466 | "/watch": ['v'], 467 | "/story.php": ['id', 'story_fbid'], 468 | }) 469 | 470 | 471 | class PixivLink(GenericWebsiteLink): 472 | """ 473 | Pixiv website. 474 | """ 475 | 476 | name = 'Pixiv' 477 | id = 'pixiv' 478 | hypertext_label = 'Pixiv' 479 | fix_domain = "phixiv.net" 480 | fixer_name = "phixiv" 481 | routes = generate_routes( 482 | "pixiv.net", 483 | { 484 | "/member_illust.php": ['illust_id'], 485 | "/:lang?/artworks/:id/:media?": None, 486 | }) 487 | 488 | 489 | class TwitchLink(GenericWebsiteLink): 490 | """ 491 | Twitch website. 492 | """ 493 | 494 | name = 'Twitch' 495 | id = 'twitch' 496 | hypertext_label = 'Twitch' 497 | fix_domain = "fxtwitch.seria.moe" 498 | fixer_name = "fxtwitch" 499 | routes = generate_routes( 500 | "twitch.tv", 501 | { 502 | "/:username/clip/:id": None, 503 | }) 504 | 505 | 506 | class SpotifyLink(GenericWebsiteLink): 507 | """ 508 | Spotify website. 509 | """ 510 | 511 | name = 'Spotify' 512 | id = 'spotify' 513 | hypertext_label = 'Spotify' 514 | fix_domain = "fxspotify.com" 515 | fixer_name = "fxspotify" 516 | routes = generate_routes( 517 | "spotify.com", 518 | { 519 | "/:lang?/track/:id": None, 520 | }) 521 | 522 | 523 | class DeviantArtLink(GenericWebsiteLink): 524 | """ 525 | DeviantArt website. 526 | """ 527 | 528 | name = 'DeviantArt' 529 | id = 'deviantart' 530 | hypertext_label = 'DeviantArt' 531 | fix_domain = "fixdeviantart.com" 532 | fixer_name = "fixDeviantArt" 533 | routes = generate_routes( 534 | "deviantart.com", 535 | { 536 | "/:username/:media_type(art|journal)/:id": None, 537 | }) 538 | 539 | 540 | class MastodonLink(GenericWebsiteLink): 541 | """ 542 | Mastodon website. 543 | """ 544 | 545 | name = 'Mastodon' 546 | id = 'mastodon' 547 | hypertext_label = 'Mastodon' 548 | fix_domain = "fx.zillanlabs.tech" 549 | fixer_name = "FxMastodon" 550 | routes = generate_routes( 551 | ["mastodon.social", "mstdn.jp", "mastodon.cloud", "mstdn.social", "mastodon.world", "mastodon.online", "mas.to", "techhub.social", "mastodon.uno", "infosec.exchange"], 552 | { 553 | "/@:username/:id": None, 554 | }) 555 | 556 | async def get_fixed_url(self) -> tuple[Optional[str], Optional[str]]: 557 | fixed_url = self.match.expand(self.repl.format(domain=self.fix_domain + r"/\g")) 558 | return fixed_url, self.fixer_name 559 | 560 | 561 | class TumblrLink(GenericWebsiteLink): 562 | """ 563 | Tumblr website. 564 | """ 565 | 566 | name = 'Tumblr' 567 | id = 'tumblr' 568 | hypertext_label = 'Tumblr' 569 | fix_domain = "tpmblr.com" 570 | fixer_name = "fxtumblr" 571 | routes = generate_routes( 572 | "tumblr.com", 573 | { 574 | "/post/:id/:slug?": None, 575 | "/:username/:id/:slug?": None, 576 | }) 577 | 578 | async def get_fixed_url(self) -> tuple[Optional[str], Optional[str]]: 579 | domain = self.fix_domain 580 | if self.match['subdomain'] and self.match['subdomain'] != 'www': 581 | domain = r"\g." + domain 582 | fixed_url = self.match.expand(self.repl.format(domain=domain)) 583 | return fixed_url, self.fixer_name 584 | 585 | async def get_author_url(self) -> tuple[Optional[str], Optional[str]]: 586 | username = self.match['username'] \ 587 | if 'username' in self.match.groupdict() else self.match['subdomain'] 588 | if not username or username == "www": 589 | return None, None 590 | user_link = f"https://{username}.tumblr.com" 591 | return user_link, username 592 | 593 | 594 | class BiliBiliLink(GenericWebsiteLink): 595 | """ 596 | BiliBili website. 597 | """ 598 | 599 | name = 'BiliBili' 600 | id = 'bilibili' 601 | hypertext_label = 'BiliBili' 602 | fix_domain = "vxbilibili.com" 603 | fixer_name = "BiliFix" 604 | routes = generate_routes( 605 | ["bilibili.com", "b23.tv"], 606 | { 607 | "/video/:id": None, 608 | "/:id": None, 609 | "/bangumi/play/:id": None, 610 | }) 611 | 612 | async def get_fixed_url(self) -> tuple[Optional[str], Optional[str]]: 613 | fix_domain = "vx" + self.match['domain'] 614 | fixed_url = self.match.expand(self.repl.format(domain=fix_domain)) 615 | return fixed_url, self.fixer_name 616 | 617 | 618 | class IFunnyLink(EmbedEZLink): 619 | """ 620 | IFunny website. 621 | """ 622 | 623 | name = 'IFunny' 624 | id = 'ifunny' 625 | hypertext_label = 'IFunny' 626 | fix_domain = "ifunnyez.co" 627 | routes = generate_routes( 628 | "ifunny.co", 629 | { 630 | "/:media_type(video|picture|gif)/:id": None, 631 | }) 632 | 633 | 634 | class FurAffinityLink(GenericWebsiteLink): 635 | """ 636 | FurAffinity website. 637 | """ 638 | 639 | name = 'Fur Affinity' 640 | id = 'furaffinity' 641 | hypertext_label = 'Fur Affinity' 642 | fix_domain = "xfuraffinity.net" 643 | fixer_name = "xfuraffinity" 644 | routes = generate_routes( 645 | "furaffinity.net", 646 | { 647 | "/view/:id": None, 648 | }) 649 | 650 | 651 | class YouTubeLink(GenericWebsiteLink): 652 | """ 653 | YouTube website. 654 | """ 655 | 656 | name = 'YouTube' 657 | id = 'youtube' 658 | hypertext_label = 'YouTube' 659 | fix_domain = "koutube.com" 660 | fixer_name = "Koutube" 661 | routes = generate_routes( 662 | ["youtube.com", "youtu.be"], 663 | { 664 | "/watch": ['v'], 665 | "/playlist": ['list'], 666 | "/shorts/:id": None, 667 | "/:id": None, 668 | }) 669 | 670 | 671 | class CustomLink(WebsiteLink): 672 | """ 673 | Custom website. 674 | """ 675 | 676 | name = 'Custom' 677 | id = 'custom' 678 | 679 | def __init__(self, guild: Guild, url: str) -> None: 680 | super().__init__(guild, url) 681 | self.fixed_link: Optional[str] = None 682 | self.hypertext_label: Optional[str] = None 683 | self.fixer_domain: Optional[str] = None 684 | 685 | # noinspection PyTypeChecker 686 | self.custom_websites: Iterable = guild.custom_websites 687 | for website in self.custom_websites: 688 | if match := re.fullmatch( 689 | fr"https?://(?:www\.)?({re.escape(website.domain)})/(.+)", self.url, re.IGNORECASE): 690 | self.fixed_link = f"https://{website.fix_domain}/{match[2]}" 691 | self.hypertext_label = website.name 692 | self.fixer_domain = website.fix_domain 693 | 694 | @classmethod 695 | def if_valid(cls, guild: Guild, url: str) -> Optional[Self]: 696 | 697 | if not guild.custom_websites: 698 | return None 699 | 700 | self = cls(guild, url) 701 | return self if self.is_valid() else None 702 | 703 | def is_valid(self) -> bool: 704 | return self.fixed_link is not None 705 | 706 | @call_if_valid 707 | async def get_fixed_url(self) -> tuple[Optional[str], Optional[str]]: 708 | if not self.fixed_link: 709 | return None, None 710 | fixer_name = self.fixer_domain 711 | fixer_name = fixer_name.split("/")[0] 712 | fixer_elements = fixer_name.split(".") 713 | if len(fixer_elements) > 1: 714 | fixer_name = ".".join(fixer_elements[:-1]) 715 | else: 716 | fixer_name = fixer_elements[0] 717 | fixer_name = fixer_name.capitalize() 718 | return self.fixed_link, fixer_name 719 | 720 | @call_if_valid 721 | async def get_author_url(self) -> tuple[Optional[str], Optional[str]]: 722 | return None, None 723 | 724 | @call_if_valid 725 | async def get_original_url(self) -> tuple[Optional[str], Optional[str]]: 726 | return self.url, self.hypertext_label 727 | 728 | 729 | websites: list[Type[WebsiteLink]] = [ 730 | TwitterLink, 731 | InstagramLink, 732 | TikTokLink, 733 | RedditLink, 734 | ThreadsLink, 735 | BlueskyLink, 736 | SnapchatLink, 737 | FacebookLink, 738 | PixivLink, 739 | TwitchLink, 740 | SpotifyLink, 741 | DeviantArtLink, 742 | MastodonLink, 743 | TumblrLink, 744 | BiliBiliLink, 745 | IFunnyLink, 746 | FurAffinityLink, 747 | YouTubeLink, 748 | CustomLink 749 | ] 750 | -------------------------------------------------------------------------------- /terms-of-service.md: -------------------------------------------------------------------------------- 1 | # Terms of Service for FixTweetBot 2 | **Effective Date:** 06/05/2025 3 | 4 | These Terms of Service ("Terms") govern your use of *FixTweetBot* ("the Bot", "we", "our") as a service provided through the Discord platform. By inviting or interacting with FixTweetBot, you agree to comply with and be bound by these Terms. 5 | 6 | --- 7 | 8 | ## 1. Acceptance of Terms 9 | 10 | By using FixTweetBot, you agree to these Terms, our [Privacy Policy](https://github.com/Kyrela/FixTweetBot/blob/main/privacy-policy.md), and Discord’s own [Terms of Service](https://discord.com/terms) and [Community Guidelines](https://discord.com/guidelines). If you do not agree with any part of these Terms, you must not use the Bot. 11 | 12 | --- 13 | 14 | ## 2. Eligibility and Permitted Use 15 | 16 | Use of the Bot is permitted only in environments that: 17 | 18 | - Comply with Discord’s Terms of Service and Community Guidelines. 19 | - Do not promote or engage in illegal activity, as defined by the laws of **France** and the **jurisdiction of the user**. 20 | - Do not involve bot abuse, spam, or excessive automation. 21 | - Respect ethical standards of conduct, including prohibitions against hate speech, harassment, and malicious activity. 22 | 23 | --- 24 | 25 | ## 3. License and Usage 26 | 27 | The Bot is made available under the terms of its applicable open-source license. Users interested in modifying, redistributing, or self-hosting the Bot should refer to the license terms provided in the GitHub repository: 28 | **[FixTweetBot License](https://github.com/Kyrela/FixTweetBot/blob/main/LICENSE)** 29 | 30 | --- 31 | 32 | ## 4. No Warranty 33 | 34 | FixTweetBot is provided **"as-is" and "as-available"** without warranties of any kind, either express or implied. We make no guarantees regarding uptime, reliability, functionality, or performance. 35 | 36 | --- 37 | 38 | ## 5. Limitation of Liability 39 | 40 | To the fullest extent permitted by law, we disclaim any liability for: 41 | 42 | - Data loss, corruption, or bot malfunction. 43 | - Content or behavior of users when using the Bot. 44 | - Indirect, incidental, special, or consequential damages resulting from use or inability to use the Bot. 45 | 46 | --- 47 | 48 | ## 6. User Responsibilities 49 | 50 | Users are solely responsible for ensuring their use of the Bot: 51 | 52 | - Complies with all relevant local, national, and international laws. 53 | - Adheres to Discord’s Terms of Service and Community Guidelines. 54 | - Does not result in abuse of the Bot or compromise its operation for others. 55 | 56 | --- 57 | 58 | ## 7. Termination 59 | 60 | We reserve the right to: 61 | 62 | - Remove the Bot from any server at any time and for any reason, with or without notice. 63 | - Immediately suspend or revoke access in cases of Terms violations. 64 | 65 | --- 66 | 67 | ## 8. Changes to the Terms 68 | 69 | We may revise these Terms from time to time. The latest version will always be available at: 70 | **[https://github.com/Kyrela/FixTweetBot/blob/main/terms-of-service.md](https://github.com/Kyrela/FixTweetBot/blob/main/terms-of-service.md)** 71 | 72 | The top of the document will reflect the date of the latest revision. Continued use of the Bot constitutes acceptance of the updated Terms. 73 | 74 | --- 75 | 76 | ## 9. Contact 77 | 78 | For questions or concerns regarding these Terms, please contact the Bot operator via: 79 | 80 | - The official GitHub repository: [FixTweetBot GitHub](https://github.com/Kyrela/FixTweetBot) 81 | - The official Discord support server: [FixTweetBot Discord](https://discord.gg/3ej9JrkF3U) 82 | - Email (as provided in bot documentation or GitHub profile) 83 | --------------------------------------------------------------------------------