From 2fd83c6818a619da86dc6fe0b51204ca0783ec6a Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 14 Nov 2025 19:51:45 +0100 Subject: [PATCH 1/6] Fix/publish docker image (#1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Test fix to for pushing to GHCR * Update circleCI template * docker(build): Updates to `golang:1.25-alpine` * docker-compose(environment): Supports ENV vars Allows building docker image using `docker compose` remotely, and adds support for passable process ENVIRONMENT variables - not failing when no dedicated .env-file is being mapped / available. While it should however retain the backwards-compatibility for still using a .env config file, but ideally using: - a) the CLI argument `--env-file /path/to/.env`, - or b) a `volumes:`-mapping on the docker compose service definition. * circleci: Uses golangci-lint v2.6.1 for `go_1-25` * circleci(go_1-25): Tries to replace codeclimate CodeClimate seems to be no longer existing, «Qlty is from the makers of Code Climate, who built the first cloud-based code quality platform in 2011.»: https://codeclimate.com/blog/code-climate-quality-is-now-qlty-software --------- Co-authored-by: Tim Zabel --- .circleci/config.yml | 45 +++++++++++---- .github/workflows/publish_docker_image.yml | 56 +++++++++---------- cmd/teleirc.go | 5 +- deployments/container/Dockerfile | 4 +- .../container/docker-compose.yml.example | 5 +- docs/user/config-file-glossary.rst | 17 +++++- docs/user/quick-start.md | 28 ++++++++-- env.example | 12 ++++ internal/config.go | 31 ++++++++-- 9 files changed, 145 insertions(+), 58 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d22e92bd..1dadb537 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,9 +9,10 @@ version: 2.1 workflows: main: jobs: - - go_1-15 - - go_1-16 - - go_1-17 + - go_1-18 + - go_1-19 + - go_1-20 + - go_1-25 - build_docs commands: @@ -19,7 +20,7 @@ commands: description: Run linter checks on TeleIRC. steps: - checkout - - run: + - run: name: Download and install golintci-lint. command: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sudo sh -s -- -b $(go env GOPATH)/bin v1.43.0 - run: @@ -34,21 +35,21 @@ commands: command: go test -coverprofile=c.out ./... jobs: - go_1-15: + go_1-18: docker: - - image: cimg/go:1.15 + - image: cimg/go:1.18 steps: - golintci-lint - teleirc-test - go_1-16: + go_1-19: docker: - - image: cimg/go:1.16 + - image: cimg/go:1.19 steps: - golintci-lint - teleirc-test - go_1-17: + go_1-20: docker: - - image: cimg/go:1.17 + - image: cimg/go:1.20 steps: - golintci-lint - run: @@ -64,6 +65,30 @@ jobs: command: | sed -i 's/github.com\/ritlug\/teleirc\///g' c.out /tmp/cc-test-reporter after-build + go_1-25: + docker: + - image: cimg/go:1.25 + steps: + - checkout + - run: + name: Download and install golintci-lint. + command: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sudo sh -s -- -b $(go env GOPATH)/bin v2.6.1 + - run: + name: Run Go linter checks. + command: golangci-lint run + - teleirc-test + - run: + name: Display test coverage summary. + command: | + go tool cover -func=c.out + echo "Total coverage:" + go tool cover -func=c.out | grep total | awk '{print $3}' + - run: + name: Generate HTML coverage report. + command: go tool cover -html=c.out -o coverage.html + - store_artifacts: + path: coverage.html + destination: coverage-report build_docs: docker: - image: cimg/python:3.10 diff --git a/.github/workflows/publish_docker_image.yml b/.github/workflows/publish_docker_image.yml index 822d48f8..bec890c6 100644 --- a/.github/workflows/publish_docker_image.yml +++ b/.github/workflows/publish_docker_image.yml @@ -1,49 +1,47 @@ -name: Build and Push Docker Image - +name: Build and Publish Docker Image + +#on: +# push: +# branches: +# - main +# tags: +# - 'v*' on: + pull_request: + branches: + - main push: - branches: [main] - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - -concurrency: - group: teleirc-docker-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + tags: + - 'v*' jobs: - build-and-push-image: - name: Build and Push Image - + build-and-push: + name: Build and Push Docker Image to GHCR runs-on: ubuntu-latest permissions: contents: read packages: write + id-token: write steps: - - name: Checkout repository + - name: Checkout code uses: actions/checkout@v4 - - name: Log in to the Container registry - uses: docker/login-action@v2 + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v4 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Set up docker buildx + uses: docker/setup-buildx-action@v3 - - name: Build and push Docker image - uses: docker/build-push-action@v4 + - name: Build and push docker image + uses: docker/build-push-action@v6 with: - push: true context: . file: ./deployments/container/Dockerfile - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + push: true + tags: | + ghcr.io/ritlug/${{ github.repository##*/ }}:${{ github.sha }} diff --git a/cmd/teleirc.go b/cmd/teleirc.go index fa8921c6..c24e5a77 100644 --- a/cmd/teleirc.go +++ b/cmd/teleirc.go @@ -6,6 +6,7 @@ import ( "os" "os/signal" "syscall" + "strconv" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" "github.com/ritlug/teleirc/internal" @@ -14,8 +15,8 @@ import ( ) var ( - flagPath = flag.String("conf", ".env", "config file") - flagDebug = flag.Bool("debug", false, "enable debugging output") + flagPath = flag.String("conf", "", "config file") + flagDebug = flag.Bool("debug", func() bool { env, _ := strconv.ParseBool(os.Getenv("DEBUG")); return env }(), "enable debugging output") flagVersion = flag.Bool("version", false, "displays current version of TeleIRC") version string ) diff --git a/deployments/container/Dockerfile b/deployments/container/Dockerfile index 633cd8ad..d813999d 100644 --- a/deployments/container/Dockerfile +++ b/deployments/container/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.22-alpine AS builder +FROM golang:1.25-alpine AS builder WORKDIR /app @@ -16,8 +16,6 @@ RUN adduser -D teleirc-user USER teleirc-user COPY --from=builder /app/teleirc /opt/teleirc/teleirc -COPY --from=builder /app/.env /opt/teleirc/conf WORKDIR /opt/teleirc ENTRYPOINT [ "./teleirc" ] -CMD [ "-conf", "/opt/teleirc/conf", "-debug", "true" ] diff --git a/deployments/container/docker-compose.yml.example b/deployments/container/docker-compose.yml.example index 59ce7db5..63797c1a 100644 --- a/deployments/container/docker-compose.yml.example +++ b/deployments/container/docker-compose.yml.example @@ -6,7 +6,6 @@ version: '3' services: teleirc: build: - context: ../../ - dockerfile: ./deployments/container/Dockerfile - env_file: ../../.env + context: https://github.com/RITlug/teleirc.git + dockerfile: deployments/container/Dockerfile user: teleirc diff --git a/docs/user/config-file-glossary.rst b/docs/user/config-file-glossary.rst index 21beffcf..04918336 100644 --- a/docs/user/config-file-glossary.rst +++ b/docs/user/config-file-glossary.rst @@ -3,8 +3,21 @@ Config file glossary #################### This page is a glossary of different settings in the ``env.example`` configuration file. -All values shown are the default settings. -This glossary is intended for advanced users. + +.. note:: + All values shown are the default settings. + This glossary is intended for advanced users. + + +************ +General settings +************ + +Configuration settings +======================== + +``DEBUG=false`` + (Optional) Verbose logging, enabled when set to `true` ************ diff --git a/docs/user/quick-start.md b/docs/user/quick-start.md index 53e2fe92..9c19560a 100644 --- a/docs/user/quick-start.md +++ b/docs/user/quick-start.md @@ -132,15 +132,35 @@ There are two ways to deploy TeleIRC persistently: Containers are the easiest way to deploy TeleIRC. Dockerfiles and other deployment resources are available in ``deployments/``. -#### Build TeleIRC +Ensure you have [docker](https://www.docker.com/) installed. + +#### Build TeleIRC docker image -1. Ensure you have [docker](https://www.docker.com/) installed 1. Enter container deployment directory (`cd deployments/container`) 1. Build image (`./build_image.sh`) 1. Run container (`docker run teleirc:latest`) -**NOTE**: -**This deployment method assumes you have a complete .env file** +> [!NOTE] +> This deployment can optionally copy a standalone .env file + + +#### Run TeleIRC using Docker compose + +1. Enter container deployment directory (`cd deployments/container`) +1. Run service using `docker compose`: + +```bash +IRC_SERVER=chat.freenode.net \ +IRC_CHANNEL='#channelname' \ +IRC_BOT_NAME='teleirc' \ +TELEIRC_TOKEN='000000000:AAAAAAaAAa2AaAAaoAAAA-a_aaAAaAaaaAA' \ +TELEGRAM_CHAT_ID='-0000000000000' \ +docker compose up -d teleirc +``` + +> [!TIP] +> Instead you can also add `environment:` entries via `docker-compose.yml`, or pass a standalone `.env` file using the CLI: +> `docker compose --env-file ../../.env up --build -d teleirc` ### Run binary diff --git a/env.example b/env.example index c9282e76..ed5650c2 100644 --- a/env.example +++ b/env.example @@ -2,6 +2,17 @@ # See the Config File Glossary for instructions. # https://docs.teleirc.com/en/latest/user/config-file-glossary/ +############################################################################### +# # +# General settings # +# # +############################################################################### + +#####----- Configuration settings -----##### +DEBUG=false + + + ############################################################################### # # # IRC configuration settings # @@ -77,6 +88,7 @@ LEAVE_MESSAGE_ALLOW_LIST="" SHOW_DISCONNECT_MESSAGE=true + ################################################################################ # # # Imgur configuration settings # diff --git a/internal/config.go b/internal/config.go index 4d22cc9d..03812633 100644 --- a/internal/config.go +++ b/internal/config.go @@ -3,6 +3,7 @@ package internal import ( "fmt" "os" + "path/filepath" "strings" "github.com/caarlos0/env/v6" @@ -135,13 +136,33 @@ func LoadConfig(path string) (*Settings, error) { if err := validate.RegisterValidation("notempty", validateEmptyString); err != nil { return nil, err } - // Attempt to load environment variables from path if path was provided - if path != ".env" && path != "" { - if err := godotenv.Load(path); err != nil { - return nil, err + // If a path was provided, try to load it. + if path != "" { + if info, err := os.Stat(path); err == nil { + // If the path is a directory, look for /.env. + if info.IsDir() { + envFile := filepath.Join(path, defaultPath) + if _, err := os.Stat(envFile); err == nil { + if err := godotenv.Load(envFile); err != nil { + return nil, err + } + } + } else { + // path exists and is a file — attempt to load it + if err := godotenv.Load(path); err != nil { + return nil, err + } + } + } else { + // If the provided path does not exist, continue and rely on passed process ENV variables + if os.IsNotExist(err) { + warning.Printf("config path %q not provided or does not exist; continuing and using process environment variables", path) + } else { + return nil, err + } } } else if _, err := os.Stat(defaultPath); !os.IsNotExist(err) { - // Attempt to load from defaultPath if defaultPath exists + // Attempt to load from defaultPath if it exists if err := godotenv.Load(defaultPath); err != nil { return nil, err } From bfd2a8806910bb524f8bb46ddcfb2ecd2ff0600a Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 14 Nov 2025 19:55:07 +0100 Subject: [PATCH 2/6] teleirc: Adds options to disable message bridges (#2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allows disabling either bridge to go just "one-way": Telegram → IRC, and IRC → Telegram Each are naturally ENABLED by default, but can be individually be disabled using: - a) the CLI arguments `--noirc` and `--notelegram`, - b) via settings env vars `DISABLE_RELAY_TO_IRC=true` and `DISABLE_RELAY_TO_TELEGRAM=true` --- cmd/teleirc.go | 15 +++++++-- docs/user/config-file-glossary.rst | 6 ++++ env.example | 2 ++ internal/handlers/irc/irc.go | 9 ++++-- internal/handlers/telegram/telegram.go | 42 +++++++++++++++----------- 5 files changed, 52 insertions(+), 22 deletions(-) diff --git a/cmd/teleirc.go b/cmd/teleirc.go index c24e5a77..1217ba66 100644 --- a/cmd/teleirc.go +++ b/cmd/teleirc.go @@ -15,8 +15,10 @@ import ( ) var ( - flagPath = flag.String("conf", "", "config file") + flagPath = flag.String("conf", ".env", "config file") flagDebug = flag.Bool("debug", func() bool { env, _ := strconv.ParseBool(os.Getenv("DEBUG")); return env }(), "enable debugging output") + flagMuteIrc = flag.Bool("muteirc", func() bool { env, _ := strconv.ParseBool(os.Getenv("DISABLE_RELAY_TO_IRC")); return env }(), "disable Telegram messages to IRC") + flagMuteTg = flag.Bool("mutetelegram", func() bool { env, _ := strconv.ParseBool(os.Getenv("DISABLE_RELAY_TO_TELEGRAM")); return env }(), "disable IRC messages to Telegram") flagVersion = flag.Bool("version", false, "displays current version of TeleIRC") version string ) @@ -34,6 +36,13 @@ func main() { // Notify that logger is enabled logger.LogDebug("Debug mode enabled!") + if *flagMuteIrc { + logger.LogInfo("Relaying messages to IRC is turned OFF!") + } + if *flagMuteTg { + logger.LogInfo("Relaying messages to Telegram is turned OFF!") + } + settings, err := internal.LoadConfig(*flagPath) if err != nil { logger.LogError(err) @@ -44,10 +53,10 @@ func main() { signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM) var tgapi *tgbotapi.BotAPI - tgClient := tg.NewClient(&settings.Telegram, &settings.IRC, &settings.Imgur, tgapi, logger) + tgClient := tg.NewClient(&settings.Telegram, &settings.IRC, &settings.Imgur, tgapi, logger, *flagMuteIrc) tgChan := make(chan error) - ircClient := irc.NewClient(&settings.IRC, &settings.Telegram, logger) + ircClient := irc.NewClient(&settings.IRC, &settings.Telegram, logger, *flagMuteTg) ircChan := make(chan error) go ircClient.StartBot(ircChan, tgClient.SendMessage) diff --git a/docs/user/config-file-glossary.rst b/docs/user/config-file-glossary.rst index 04918336..95eb9512 100644 --- a/docs/user/config-file-glossary.rst +++ b/docs/user/config-file-glossary.rst @@ -19,6 +19,12 @@ Configuration settings ``DEBUG=false`` (Optional) Verbose logging, enabled when set to `true` +``DISABLE_RELAY_TO_IRC=false`` + (Optional) Fully disables bridging messages from Telegram → IRC when set to `true` + +``DISABLE_RELAY_TO_TELEGRAM=false`` + (Optional) Fully disables bridging messages from IRC → Telegram when set to `true` + ************ IRC settings diff --git a/env.example b/env.example index ed5650c2..3c133874 100644 --- a/env.example +++ b/env.example @@ -10,6 +10,8 @@ #####----- Configuration settings -----##### DEBUG=false +DISABLE_RELAY_TO_IRC=false +DISABLE_RELAY_TO_TELEGRAM=false diff --git a/internal/handlers/irc/irc.go b/internal/handlers/irc/irc.go index ccbccce9..c2d6947a 100644 --- a/internal/handlers/irc/irc.go +++ b/internal/handlers/irc/irc.go @@ -18,12 +18,13 @@ type Client struct { TelegramSettings *internal.TelegramSettings logger internal.DebugLogger sendToTg func(string) + disableTgRelay bool } /* NewClient returns a new IRCClient based on the provided settings */ -func NewClient(settings *internal.IRCSettings, telegramSettings *internal.TelegramSettings, logger internal.DebugLogger) Client { +func NewClient(settings *internal.IRCSettings, telegramSettings *internal.TelegramSettings, logger internal.DebugLogger, disableTgRelay bool) Client { logger.LogInfo("Creating new IRC bot client...") client := girc.New(girc.Config{ Server: settings.Server, @@ -52,7 +53,7 @@ func NewClient(settings *internal.IRCSettings, telegramSettings *internal.Telegr } } - return Client{client, settings, telegramSettings, logger, nil} + return Client{client, settings, telegramSettings, logger, nil, disableTgRelay} } /* @@ -120,6 +121,10 @@ func (c Client) Logger() internal.DebugLogger { SendToTg sends a message to Telegram */ func (c Client) SendToTg(msg string) { + if c.disableTgRelay { + c.logger.LogDebug("Relaying to Telegram is disabled, skipping IRC message") + return + } c.sendToTg(msg) } diff --git a/internal/handlers/telegram/telegram.go b/internal/handlers/telegram/telegram.go index 1479f86a..aa295813 100644 --- a/internal/handlers/telegram/telegram.go +++ b/internal/handlers/telegram/telegram.go @@ -11,20 +11,21 @@ Client contains information for the Telegram bridge, including the TelegramSettings needed to run the bot */ type Client struct { - api *tgbotapi.BotAPI - Settings *internal.TelegramSettings - IRCSettings *internal.IRCSettings - ImgurSettings *internal.ImgurSettings - logger internal.DebugLogger - sendToIrc func(string) + api *tgbotapi.BotAPI + Settings *internal.TelegramSettings + IRCSettings *internal.IRCSettings + ImgurSettings *internal.ImgurSettings + logger internal.DebugLogger + sendToIrc func(string) + disableIrcRelay bool } /* NewClient creates a new Telegram bot client */ -func NewClient(settings *internal.TelegramSettings, ircsettings *internal.IRCSettings, imgur *internal.ImgurSettings, tgapi *tgbotapi.BotAPI, logger internal.DebugLogger) *Client { +func NewClient(settings *internal.TelegramSettings, ircsettings *internal.IRCSettings, imgur *internal.ImgurSettings, tgapi *tgbotapi.BotAPI, logger internal.DebugLogger, disableIrcRelay bool) *Client { logger.LogInfo("Creating new Telegram bot client...") - return &Client{api: tgapi, Settings: settings, IRCSettings: ircsettings, ImgurSettings: imgur, logger: logger} + return &Client{api: tgapi, Settings: settings, IRCSettings: ircsettings, ImgurSettings: imgur, logger: logger, disableIrcRelay: disableIrcRelay} } /* @@ -66,16 +67,23 @@ func (tg *Client) StartBot(errChan chan<- error, sendMessage func(string)) { tg.logger.LogInfo("Authorized on account", tg.api.Self.UserName) tg.sendToIrc = sendMessage - u := tgbotapi.NewUpdate(0) - u.Timeout = 60 + if !tg.disableIrcRelay { + u := tgbotapi.NewUpdate(0) + u.Timeout = 60 - updates, err := tg.api.GetUpdatesChan(u) - if err != nil { - errChan <- err - tg.logger.LogError(err) - } + updates, err := tg.api.GetUpdatesChan(u) + if err != nil { + errChan <- err + tg.logger.LogError(err) + return + } - updateHandler(tg, updates) + updateHandler(tg, updates) - errChan <- nil + errChan <- nil + } else { + tg.logger.LogInfo("Telegram -> IRC relay disabled, but Telegram bot remains active for IRC -> Telegram messages") + // Block forever to keep the goroutine alive + select {} + } } From 8b40f0090f8505c2ac9c6825975aa9ce1c384884 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 14 Nov 2025 19:55:47 +0100 Subject: [PATCH 3/6] Feature/quotestyle irc nicknames (#3) * telegram: Adds option to put IRC nickname in quote Introduces a new optional `QUOTE_NICKNAME=true` setting, that adjusts how IRC messages - particularly the nickname part - are rendered when bridged to Telegram: instead of a whole unformatted string, it uses the supported `
`-formatting for the Telegram messages. This results in the sending IRC nickname to be better visible, but also clearly distinguishable from the IRC user's effective Text Message. - Telegram message default behaviour / before: ` Hello from IRC to Telegram` - New behaviour and format, if this mode is enabled, will rather look as follows: ``` > Hello from IRC to Telegram ``` For this to work: - the message's parse_mode has to be set to `HTML` - and this requires proper HTML-escaping / HTML-tag removal on the IRC nickname & message parts, before calling the Telegram Bot API sendMessage command. - The StripHTML() func invoked for that, was taken from here: https://go.dev/play/p/fqHzlCJa9ta (L31-35) * irc(messageHandler): Adds also html.EscapeString() + A few Go syntax validation improvements --- docs/user/config-file-glossary.rst | 3 +++ env.example | 1 + internal/config.go | 1 + internal/handlers/irc/handlers.go | 9 ++++++++- internal/handlers/telegram/telegram.go | 3 +++ 5 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/user/config-file-glossary.rst b/docs/user/config-file-glossary.rst index 95eb9512..57672b67 100644 --- a/docs/user/config-file-glossary.rst +++ b/docs/user/config-file-glossary.rst @@ -190,6 +190,9 @@ Telegram settings ``SHOW_DISCONNECT_MESSAGE=true`` Sends a message to Telegram when the bot disconnects from the IRC side. +``QUOTE_NICKNAME=false`` + Place IRC nickname in a blockquote section of the message to Telegram, instead of inline message prefix. + ************** Imgur settings ************** diff --git a/env.example b/env.example index 3c133874..10f5cb46 100644 --- a/env.example +++ b/env.example @@ -88,6 +88,7 @@ SHOW_NICK_MESSAGE=false SHOW_LEAVE_MESSAGE=false LEAVE_MESSAGE_ALLOW_LIST="" SHOW_DISCONNECT_MESSAGE=true +QUOTE_NICKNAME=false diff --git a/internal/config.go b/internal/config.go index 03812633..7acd0ccb 100644 --- a/internal/config.go +++ b/internal/config.go @@ -66,6 +66,7 @@ type TelegramSettings struct { ShowNickMessage bool `env:"SHOW_NICK_MESSAGE" envDefault:"false"` ShowDisconnectMessage bool `env:"SHOW_DISCONNECT_MESSAGE" envDefault:"false"` MaxMessagePerMinute int `env:"MAX_MESSAGE_PER_MINUTE" envDefault:"20"` + QuoteNick bool `env:"QUOTE_NICKNAME" envDefault:"false"` } // ImgurSettings includes settings related to Imgur uploading for Telegram photos diff --git a/internal/handlers/irc/handlers.go b/internal/handlers/irc/handlers.go index be771308..5289d25c 100644 --- a/internal/handlers/irc/handlers.go +++ b/internal/handlers/irc/handlers.go @@ -2,6 +2,7 @@ package irc import ( "fmt" + "html" "regexp" "strings" @@ -129,7 +130,13 @@ func messageHandler(c ClientInterface) func(*girc.Client, girc.Event) { // Strips out ACTION word from text formatted = "* " + e.Source.Name + msg[7:len(msg)-1] } else { - formatted = c.IRCSettings().Prefix + e.Source.Name + c.IRCSettings().Suffix + " " + e.Params[1] + if c.TgSettings().QuoteNick { + ircNicknameFormatted := c.IRCSettings().Prefix + e.Source.Name + c.IRCSettings().Suffix + ircMessageNoHtml := regexp.MustCompile(`<.*?>`).ReplaceAllString(e.Params[1], "") + formatted = "
" + html.EscapeString(ircNicknameFormatted) + "
\n" + html.EscapeString(strings.NewReplacer(">", "", "<", "").Replace(ircMessageNoHtml)) + } else { + formatted = c.IRCSettings().Prefix + e.Source.Name + c.IRCSettings().Suffix + " " + e.Params[1] + } } if hasNoForwardPrefix(c, e.Params[1]) { diff --git a/internal/handlers/telegram/telegram.go b/internal/handlers/telegram/telegram.go index aa295813..c8618a87 100644 --- a/internal/handlers/telegram/telegram.go +++ b/internal/handlers/telegram/telegram.go @@ -34,6 +34,9 @@ SendMessage sends a message to the Telegram channel specified in the settings func (tg *Client) SendMessage(msg string) { newMsg := tgbotapi.NewMessage(tg.Settings.ChatID, "") newMsg.Text = msg + if tg.Settings.QuoteNick { + newMsg.ParseMode = "HTML" + } if _, err := tg.api.Send(newMsg); err != nil { var attempts int = 0 From 0a682767c7c4ca92d9de52616bd35cc0bf287795 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 14 Nov 2025 19:57:25 +0100 Subject: [PATCH 4/6] irc: Allows to not expose Telegram usernames (#4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Telegram the username is a must-have when creating an account, whereas «First name» (and «Last name») are freely adjustable at any time. By adding a new setting `PREFER_FIRSTNAME=true` it is possible to obfuscate the Telegram user's actual \@usernames - preventing potential leakage of them to IRC channels. --- docs/user/config-file-glossary.rst | 4 ++++ env.example | 1 + internal/config.go | 1 + internal/handlers/telegram/handler.go | 18 +++++++++--------- internal/handlers/telegram/helpers.go | 14 ++++++++++---- 5 files changed, 25 insertions(+), 13 deletions(-) diff --git a/docs/user/config-file-glossary.rst b/docs/user/config-file-glossary.rst index 57672b67..c7bec9ba 100644 --- a/docs/user/config-file-glossary.rst +++ b/docs/user/config-file-glossary.rst @@ -190,6 +190,10 @@ Telegram settings ``SHOW_DISCONNECT_MESSAGE=true`` Sends a message to Telegram when the bot disconnects from the IRC side. +``PREFER_FIRSTNAME=false`` + Prefer users adjustable «First name» from Telegram, over their @usernames, when sending messages to IRC channel + (Fallback will still be the @username if first name is not available) + ``QUOTE_NICKNAME=false`` Place IRC nickname in a blockquote section of the message to Telegram, instead of inline message prefix. diff --git a/env.example b/env.example index 10f5cb46..48ca44f9 100644 --- a/env.example +++ b/env.example @@ -88,6 +88,7 @@ SHOW_NICK_MESSAGE=false SHOW_LEAVE_MESSAGE=false LEAVE_MESSAGE_ALLOW_LIST="" SHOW_DISCONNECT_MESSAGE=true +PREFER_FIRSTNAME=false QUOTE_NICKNAME=false diff --git a/internal/config.go b/internal/config.go index 7acd0ccb..a37628a8 100644 --- a/internal/config.go +++ b/internal/config.go @@ -66,6 +66,7 @@ type TelegramSettings struct { ShowNickMessage bool `env:"SHOW_NICK_MESSAGE" envDefault:"false"` ShowDisconnectMessage bool `env:"SHOW_DISCONNECT_MESSAGE" envDefault:"false"` MaxMessagePerMinute int `env:"MAX_MESSAGE_PER_MINUTE" envDefault:"20"` + PreferName bool `env:"PREFER_FIRSTNAME" envDefault:"false"` QuoteNick bool `env:"QUOTE_NICKNAME" envDefault:"false"` } diff --git a/internal/handlers/telegram/handler.go b/internal/handlers/telegram/handler.go index 021f49d1..fa0b3acc 100644 --- a/internal/handlers/telegram/handler.go +++ b/internal/handlers/telegram/handler.go @@ -59,7 +59,7 @@ messageHandler handles the Message Telegram Object, which formats the Telegram update into a simple string for IRC. */ func messageHandler(tg *Client, u tgbotapi.Update) { - username := GetUsername(tg.IRCSettings.ShowZWSP, u.Message.From) + username := GetUsername(tg.IRCSettings.ShowZWSP, u.Message.From, tg.Settings.PreferName) formatted := "" if tg.IRCSettings.NoForwardPrefix != "" && strings.HasPrefix(u.Message.Text, tg.IRCSettings.NoForwardPrefix) { @@ -93,8 +93,8 @@ replyHandler handles when users reply to a Telegram message */ func replyHandler(tg *Client, u tgbotapi.Update) { replyText := strings.Trim(u.Message.ReplyToMessage.Text, " ") - username := GetUsername(tg.IRCSettings.ShowZWSP, u.Message.From) - replyUser := GetUsername(tg.IRCSettings.ShowZWSP, u.Message.ReplyToMessage.From) + username := GetUsername(tg.IRCSettings.ShowZWSP, u.Message.From, tg.Settings.PreferName) + replyUser := GetUsername(tg.IRCSettings.ShowZWSP, u.Message.ReplyToMessage.From, tg.Settings.PreferName) // Only show a portion of the reply text if replyTextAsRunes := []rune(replyText); len(replyTextAsRunes) > tg.Settings.ReplyLength { @@ -121,7 +121,7 @@ func joinHandler(tg *Client, users *[]tgbotapi.User) { if tg.IRCSettings.ShowJoinMessage { for _, user := range *users { user := user - username := GetFullUsername(tg.IRCSettings.ShowZWSP, &user) + username := GetFullUsername(tg.IRCSettings.ShowZWSP, &user, tg.Settings.PreferName) formatted := username + " has joined the Telegram Group!" tg.sendToIrc(formatted) } @@ -133,7 +133,7 @@ partHandler handles when users leave the Telegram group */ func partHandler(tg *Client, user *tgbotapi.User) { if tg.IRCSettings.ShowLeaveMessage { - username := GetFullUsername(tg.IRCSettings.ShowZWSP, user) + username := GetFullUsername(tg.IRCSettings.ShowZWSP, user, tg.Settings.PreferName) formatted := username + " has left the Telegram Group!" tg.sendToIrc(formatted) @@ -145,7 +145,7 @@ stickerHandler handles the Message.Sticker Telegram Object, which formats the Telegram message into its base Emoji unicode character. */ func stickerHandler(tg *Client, u tgbotapi.Update) { - username := GetUsername(tg.IRCSettings.ShowZWSP, u.Message.From) + username := GetUsername(tg.IRCSettings.ShowZWSP, u.Message.From, tg.Settings.PreferName) formatted := fmt.Sprintf("%s%s%s %s", tg.Settings.Prefix, username, @@ -160,7 +160,7 @@ exists, and sends notification to IRC */ func photoHandler(tg *Client, u tgbotapi.Update) { link := uploadImage(tg, u) - username := GetUsername(tg.IRCSettings.ShowZWSP, u.Message.From) + username := GetUsername(tg.IRCSettings.ShowZWSP, u.Message.From, tg.Settings.PreferName) caption := u.Message.Caption if caption == "" { caption = "No caption provided." @@ -180,7 +180,7 @@ documentHandler receives a document object from Telegram, and sends a notification to IRC. */ func documentHandler(tg *Client, u *tgbotapi.Message) { - username := GetUsername(tg.IRCSettings.ShowZWSP, u.From) + username := GetUsername(tg.IRCSettings.ShowZWSP, u.From, tg.Settings.PreferName) formatted := username + " shared a file" if u.Document.MimeType != "" { formatted += " (" + u.Document.MimeType + ")" @@ -204,7 +204,7 @@ func locationHandler(tg *Client, u *tgbotapi.Message) { return } - username := GetUsername(tg.IRCSettings.ShowZWSP, u.From) + username := GetUsername(tg.IRCSettings.ShowZWSP, u.From, tg.Settings.PreferName) formatted := username + " shared their location: (" // f means do not use an exponent. diff --git a/internal/handlers/telegram/helpers.go b/internal/handlers/telegram/helpers.go index 19b65ab7..6973593f 100644 --- a/internal/handlers/telegram/helpers.go +++ b/internal/handlers/telegram/helpers.go @@ -5,9 +5,12 @@ import ( ) /* -GetUsername takes showZWSP condition and user then returns username with or without ​. +GetUsername takes showZWSP and preferFirstname conditions and user then returns First name or username with or without ​. */ -func GetUsername(showZWSP bool, u *tgbotapi.User) string { +func GetUsername(showZWSP bool, u *tgbotapi.User, preferFirstname bool) string { + if preferFirstname && u.FirstName != "" { + return u.FirstName + } if u.UserName == "" { return u.FirstName } @@ -18,9 +21,12 @@ func GetUsername(showZWSP bool, u *tgbotapi.User) string { } /* -GetFullUsername takes showZWSP condition and user then returns full username with or without ​. +GetFullUsername takes showZWSP and preferFirstname conditions and user then returns full name or username with or without ​. */ -func GetFullUsername(showZWSP bool, u *tgbotapi.User) string { +func GetFullUsername(showZWSP bool, u *tgbotapi.User, preferFirstname bool) string { + if preferFirstname && u.FirstName != "" { + return u.FirstName + } if u.UserName == "" { return u.FirstName } From 29b41f46aec04c9ee1e3ec21054ec7b1a0a97fcf Mon Sep 17 00:00:00 2001 From: oliveratgithub Date: Mon, 1 Dec 2025 23:44:14 +0100 Subject: [PATCH 5/6] telegram(imgur): Adds .env option to disable Imgur Additional changes: - telegram `stickerHandler`: disabled if existing .env option is `false` - telegram `documentHandler`: - disabled if existing .env option is `false` - also changed the .env option by default to OFF (`false`), following to the docs remark - telegram `locationHandler`: adds Debug log entry if skipped - Updates the relevant User docs --- docs/user/config-file-glossary.rst | 3 +++ docs/user/quick-start.md | 14 ++++++++++---- env.example | 1 + internal/config.go | 3 ++- internal/handlers/telegram/handler.go | 16 ++++++++++++++++ 5 files changed, 32 insertions(+), 5 deletions(-) diff --git a/docs/user/config-file-glossary.rst b/docs/user/config-file-glossary.rst index 21beffcf..9e65f0a5 100644 --- a/docs/user/config-file-glossary.rst +++ b/docs/user/config-file-glossary.rst @@ -97,6 +97,9 @@ Message settings ``IRC_SEND_DOCUMENT=false`` Send documents and files from Telegram to IRC (`why is this false by default? `_) +``IRC_SEND_PHOTO=true`` + All photos which the Telegram Bot receives are uploaded to imgur, and an imgur-link is then posted to IRC + ``IRC_EDITED_PREFIX="(edited) "`` Prefix to prepend to messages when a user edits a Telegram message and it is resent to IRC diff --git a/docs/user/quick-start.md b/docs/user/quick-start.md index 53e2fe92..2933ed83 100644 --- a/docs/user/quick-start.md +++ b/docs/user/quick-start.md @@ -101,17 +101,23 @@ If your IRC channel is on the Freenode IRC network, use these exact commands to 1. `/query ChanServ ACCESS #channel ADD *!*@freenode/staff/* +Aiotv` 1. `/query ChanServ ACCESS #channel ADD +V` -### Configure Imgur Image Upload (IIU) +### Adjust default Imgur Image Upload (IIU) -_By default_, TeleIRC uploads images sent to the Telegram group to [Imgur][7]. Since IRC does not support images, Imgur is an intermediary approach to sending pictures sent on Telegram over to IRC. -Note that images will be publicly visible on the Internet if the URL is known. -[See context][8] for why Imgur is enabled by default. + +> [!IMPORTANT] +> _By default_, all images the Telegram Bot reads are uploaded by TeleIRC to [Imgur][7]. +> [See context][8] for why Imgur upload is enabled by default. By default, TeleIRC uses the TeleIRC-registered Imgur API key. We highly recommend registering your own API key in high-traffic channels. Otherwise, API rate limiting can occur. +#### Optionally disable all Imgur image uploads from Telegram + +* Set `IRC_SEND_PHOTO` to `false` in your `.env` file + +#### Alternatively use your own Imgur API details To register your own Imgur API key, follow these steps: 1. Create an Imgur account diff --git a/env.example b/env.example index c9282e76..f7384606 100644 --- a/env.example +++ b/env.example @@ -45,6 +45,7 @@ IRC_PREFIX="<" IRC_SUFFIX=">" IRC_SEND_STICKER_EMOJI=true IRC_SEND_DOCUMENT=false +IRC_SEND_PHOTO=true IRC_EDITED_PREFIX="(edited) " IRC_MAX_MESSAGE_LENGTH=400 IRC_SHOW_ZWSP=true diff --git a/internal/config.go b/internal/config.go index 4d22cc9d..ed5cb51b 100644 --- a/internal/config.go +++ b/internal/config.go @@ -28,7 +28,8 @@ type IRCSettings struct { BotName string `env:"IRC_BOT_REALNAME" envDefault:"Powered by TeleIRC "` BotNick string `env:"IRC_BOT_NAME,required" validate:"notempty"` SendStickerEmoji bool `env:"IRC_SEND_STICKER_EMOJI" envDefault:"true"` - SendDocument bool `env:"IRC_SEND_DOCUMENT" envDefault:"true"` + SendDocument bool `env:"IRC_SEND_DOCUMENT" envDefault:"false"` + SendPhoto bool `env:"IRC_SEND_PHOTO" envDefault:"true"` Prefix string `env:"IRC_PREFIX" envDefault:"<"` Suffix string `env:"IRC_SUFFIX" envDefault:">"` ShowJoinMessage bool `env:"IRC_SHOW_JOIN_MESSAGE" envDefault:"true"` diff --git a/internal/handlers/telegram/handler.go b/internal/handlers/telegram/handler.go index 021f49d1..6de4c7d1 100644 --- a/internal/handlers/telegram/handler.go +++ b/internal/handlers/telegram/handler.go @@ -145,6 +145,11 @@ stickerHandler handles the Message.Sticker Telegram Object, which formats the Telegram message into its base Emoji unicode character. */ func stickerHandler(tg *Client, u tgbotapi.Update) { + if !tg.IRCSettings.SendStickerEmoji { + tg.logger.LogDebug("Skipped processing Message.Sticker. Reason: IRC_SEND_STICKER_EMOJI=false") + return + } + username := GetUsername(tg.IRCSettings.ShowZWSP, u.Message.From) formatted := fmt.Sprintf("%s%s%s %s", tg.Settings.Prefix, @@ -159,6 +164,11 @@ photoHandler handles the Message.Photo Telegram object. Only acknowledges Photo exists, and sends notification to IRC */ func photoHandler(tg *Client, u tgbotapi.Update) { + if !tg.IRCSettings.SendPhoto { + tg.logger.LogDebug("Skipped processing Message.Photo. Reason: IRC_SEND_PHOTO=false") + return + } + link := uploadImage(tg, u) username := GetUsername(tg.IRCSettings.ShowZWSP, u.Message.From) caption := u.Message.Caption @@ -180,6 +190,11 @@ documentHandler receives a document object from Telegram, and sends a notification to IRC. */ func documentHandler(tg *Client, u *tgbotapi.Message) { + if !tg.IRCSettings.SendDocument { + tg.logger.LogDebug("Skipped processing document object. Reason: IRC_SEND_DOCUMENT=false") + return + } + username := GetUsername(tg.IRCSettings.ShowZWSP, u.From) formatted := username + " shared a file" if u.Document.MimeType != "" { @@ -201,6 +216,7 @@ a notification to IRC. */ func locationHandler(tg *Client, u *tgbotapi.Message) { if !tg.IRCSettings.ShowLocationMessage { + tg.logger.LogDebug("Skipped processing location object. Reason: IRC_SHOW_LOCATION_MESSAGE=false") return } From ac1b4371e82917160db0f3fcbcec5015e59b60b1 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 2 Dec 2025 00:40:49 +0100 Subject: [PATCH 6/6] telegram(updateHandler): Restricts message access (#5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Discards any Messages form non-bridged Telegram Chat IDs the Bot «can read», but are not in scope. Better early prevention to process any arbitrary messages or objects. Allows reusing an existing Telegram Bot, which is connected to multiple Chats. Could also make the last step of the Quick Start Guide «Create bot with BotFather» obsolete / less critical: ~Send /setjoingroups to @BotFather, change to Disable~ --- internal/handlers/telegram/handler.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/internal/handlers/telegram/handler.go b/internal/handlers/telegram/handler.go index fa0b3acc..d6134e2e 100644 --- a/internal/handlers/telegram/handler.go +++ b/internal/handlers/telegram/handler.go @@ -21,6 +21,13 @@ which handler to fire off */ func updateHandler(tg *Client, updates tgbotapi.UpdatesChannel) { for u := range updates { + // Don't process any messages that didn't come from the + // chat we're bridging + if u.Message.Chat.ID != tg.Settings.ChatID { + tg.logger.LogDebug("Ignored message from a telegram chat we're not bridging:", tg.Settings.ChatID) + continue + } + switch { case u.Message == nil: tg.logger.LogError("Missing message data") @@ -66,12 +73,6 @@ func messageHandler(tg *Client, u tgbotapi.Update) { return } - // Don't forward messages to IRC that didn't come from the - // chat we're bridging - if u.Message.Chat.ID != tg.Settings.ChatID { - return - } - // Telegram user replied to a message if u.Message.ReplyToMessage != nil { replyHandler(tg, u)