Skip to content

Conversation

@firasrg
Copy link
Member

@firasrg firasrg commented Jan 13, 2026

New Feature: Rewrite Command

The /rewrite command allows users to have their messages rewritten in a clearer, more professional, or better structured form using ChatGPT AI (GPT 5 mini). The rewritten message is displayed as an ephemeral message (visible only to the user), making it perfect for getting quick writing improvements without cluttering the channel where they are.

Usage

/rewrite message:<text> [tone:<style>]

Parameters

  • message (required): The message to rewrite (3-500 characters)
  • tone (optional): The tone/style enum value for the rewrite. Defaults to CLEAR if not provided

Available Tones

  • CLEAR (default): Make the message clear and easy to understand
  • PRO: Use a professional and polished tone
  • DETAILED: Expand with more detail and explanation
  • TECHNICAL: Use technical and specialized language where appropriate

How It Works

  1. You invoke the command with your message and optional tone
  2. ChatGPT analyzes and rewrites the message according to the selected tone
  3. A formatted embed displays both the original and rewritten versions
  4. The response is ephemeral—only visible to you for 15 minutes
  5. You can copy the rewritten text and use it.

Example

image

UPDATE 16 Jan 2026

  • The command name has been changed into /rewrite-msg for consistency with the class in source code.
  • The following is a demo that shows all possible states :

tjbot-rewrite-demo-3

UPDATE 20 Jan 2026

  • The command is back to its original name /rewrite.
  • The Tone Pro has been renamed to Professionnal
  • The original msg is no more provided by the bot, since it's possible to see it through a link to the command (built-in discord feat, see the demo below)
  • New method added to ChatGptService API, askRaw to avoid touching existing method and for future reusabilities

tjbot-rewrite-demo-5

@firasrg firasrg requested a review from a team as a code owner January 13, 2026 21:56
Copy link
Contributor

@tj-wazei tj-wazei left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see comments. There's a lot of final abuse that does nothing but add clutter to the code. It's best to use final when you need it.

return;
}

final String response = buildResponse(userMessage, rewrittenMessage.orElseThrow(), tone);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary use of orElseThrow since it will never throw. You are doing an isEmpty check above.

Ideally, this entire block is a good candidate for rewrittenMessage.ifPresentOrElse() if you want that fluency.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to be fair, if (foo.isEmpty()) ... foo.orElseThrow() is the normal way ur supposed to deal with this in case you are not able to utilize the other, better, methods of the API. (but in this case, try to put the extraction of the content right after the if-check, not 20 lines later)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

u missed the point on this one @firasrg. all u did now was swapping orElseThrow with get, thats not an improvement. orElseThrow is better.

what wazei was suggesting is that u should prefer one of the "this or that" methods from the Optional-API over "if ... return ... orElseThrow".

So you should use sth like ifPresentOrElse(...) or ifPresent(...) (check out the API for a good fit) instead of

if (maybeFoo.isEmpty()) {
  ...
  return;
}
Foo foo = maybeFoo.orElseThrow();
...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Zabuzard I think it's solved with #1378 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@firasrg either make it .get() or perhaps the following:

rewrittenMessage.ifPresentOrElse(
    rewrittenText -> {
        event.getHook().editOriginal(rewrittenText).queue();
    },
    () -> {
        logger.debug("Failed to obtain a response for /{}, original message: '{}'", COMMAND_NAME, userMessage);

        event.getHook()
             .editOriginal("An error occurred while processing your request. Please try again later.")
             .queue();
    }
);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just don't want to see your code suggesting a failure path that can’t happen

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want, you can also remove Optional entirely, and inside rewrite

return chatGptService.askRaw(rewritePrompt, aiModel).orElse(""); // null, empty string whatever

then your logic could be:

if(rewrittenMessage.isBlank()) {
 logger.debug("Failed to obtain a response for /{}, original message: '{}'",
                    COMMAND_NAME, userMessage);

            event.getHook()
                .editOriginal(
                        "An error occurred while processing your request. Please try again later.")
                .queue();

            return;
} 

logger.debug("Rewrite successful; rewritten message length: {}", rewrittenMessage.length());

event.getHook().sendMessage(rewrittenMessage).setEphemeral(true).queue();

private Optional<String> rewrite(String userMessage, MsgTone tone) {
final String rewritePrompt = buildChatGptPrompt(userMessage, tone);

return chatGptService.ask(rewritePrompt, tone.displayName, CHAT_GPT_MODEL);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll need to update the ask method. The original implementor of the ChatGptService did not make this abstract enough so your request will be mixed with the following prefixed:

For code supplied for review, refer to the old code supplied rather than  rewriting the code. DON'T supply a corrected version of the code.

KEEP IT CONCISE, NOT MORE THAN 280 WORDS

%s

Question: %s

@Override
public void onSlashCommand(SlashCommandInteractionEvent event) {
final String userMessage =
Objects.requireNonNull(event.getOption(MESSAGE_OPTION)).getAsString();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for requireNonNull

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the getOption() return is Nullable based on its docs

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, but in this case its a required parameter, so it wont be null.

Also even if it was null, this is the worst way to handle it

Copy link
Member

@Zabuzard Zabuzard Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the api declares it as nullable and thats correct. but the question here is: what do you expect to get?
since u declared this parameter as required beforehand, it must not be null. if it is null, there is a severe bug in JDA and the bot should crash! (fail-fast).

so what you want is "crash if null". and for that you can either write Objects.requireNonNull(foo).bar() or simply just foo.bar(). Both are valid options to handle it and you can make an argument for either option.

we have had this discussion roughly two years back with a bigger audience and decided that Objects.requireNonNull adds too much visual clutter for this and foo.bar() is generally preferred.

but ultimately its a bit NIT.

situations where Objects.requireNonNull is really important to use though is when you do not have a situation like foo.bar() but bar(foo), as in this case a NPE wouldnt be thrown instantly. so bar(Objects.requireNonNull(foo)) would be reasonable in that situation to achieve fail-fast.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to fail fast without requireNonNull(), we grab the required message option once, throw if it’s unexpectedly missing (using the MESSAGE_OPTION in the message), then call getAsString()

Copy link
Member

@Zabuzard Zabuzard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great stuff. Apart from what the others mentioned, im happy now 👍

Comment on lines 164 to 175
if (response.length() <= Message.MAX_CONTENT_LENGTH) {
return attempt;
}

logger.debug("Rewritten message exceeded {} characters; retrying with stricter constraint",
Message.MAX_CONTENT_LENGTH);

final String shortenPrompt = rewritePrompt
+ "\n\nConstraint reminder: Your previous rewrite exceeded "
+ Message.MAX_CONTENT_LENGTH
+ " characters. Provide a revised rewrite strictly under "
+ Message.MAX_CONTENT_LENGTH + " characters while preserving meaning and tone.";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use the MAX_MESSAGE_LENGTH constant defined in the class in place of Message.MAX_CONTENT_LENGTH where appropriate.

@Together-Java Together-Java deleted a comment from Zabuzard Jan 20, 2026
@Together-Java Together-Java deleted a comment from Zabuzard Jan 20, 2026
@sonarqubecloud
Copy link

@firasrg firasrg added the new command Add a new command or group of commands to the bot label Jan 20, 2026
Copy link
Contributor

@tj-wazei tj-wazei left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost there, I still really hate the usage of final, my eyes kept having to "jump" as I was reading your code.

.model(chatModel.toChatModel())
.input(inputPrompt)
.input(prompt)
.maxOutputTokens(MAX_TOKENS)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we're here now, something we've learned is that the MAX_TOKENS should not exceed 2000. Please can you update MAX_TOKENS to equal Message.MAX_CONTENT_LENGTH

- Improve clarity and structure
- Maintain the original meaning
- Avoid em-dashes (—)
- Stay under %d characters (strict limit)
Copy link
Contributor

@tj-wazei tj-wazei Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stay under %d characters (strict limit) can be enforced via the MAX_TOKENS field in ChatGptService. Consider adding a parameter to the ask method. If doing so, ensure to validate that the value <= Message.MAX_TOKENS

private static String createAiPrompt(String userMessage, MessageTone tone) {
return """
You are rewriting a Discord text chat message for clarity and professionalism.
Keep it conversational and casual—NOT email or formal document format.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep it conversational and casual, not email or formal document format.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, let's move this out into a constant e.g.

private static final String AI_REWRITE_PROMPT_TEMPLATE = """
        You are rewriting a Discord text chat message for clarity and professionalism.
        Keep it conversational and casual, not email or formal document format.

        Tone: %s

        Rewrite the message to:
        - Improve clarity and structure
        - Maintain the original meaning
        - Avoid em-dashes (—)
        - Stay under %d characters (strict limit)

        If the message is already well-written, make only minor improvements.

        Message to rewrite:
        %s
        """.stripIndent();

...

private static String createAiPrompt(String userMessage, MessageTone tone) {
    return AI_REWRITE_PROMPT_TEMPLATE.formatted(
            tone.description,
            MAX_MESSAGE_LENGTH,
            userMessage
    );
}

private final ChatGptService chatGptService;

private static ChatGptModel selectAiModel(MessageTone tone) {
return switch (tone) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: Just add the ChatGptModel to the enum and remove this entire function.


private final ChatGptService chatGptService;

private static ChatGptModel selectAiModel(MessageTone tone) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: can we move all private static functions to the bottom of the class please? Naturally, we expect to see the constructor first.

final OptionMapping messageOption = event.getOption(MESSAGE_OPTION);

if (messageOption == null) {
throw new IllegalStateException("Required option '" + MESSAGE_OPTION + "' is missing");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: technically correct but semantically, I'd prefer IllegalArgumentException.

return MessageTone.CLEAR;
}

final String toneValue = toneOption.getAsString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be inlined i.e. return MessageTone.valueOf(toneOption.getAsString());

return attempt;
}

logger.debug("Rewritten message exceeded {} characters; retrying with stricter constraint",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rest of this code can be removed if you use the max token parameter.

This method can then be:

private Optional<String> rewrite(String userMessage, MessageTone tone) {
  return chatGptService.askRaw(createAiPrompt(userMessage, tone), selectAiModel(tone)); // if updating askRaw pass the MAX_CONTENT_LENGTH as a parameter
}

return chatGptService.askRaw(shortenPrompt, aiModel);
}

private enum MessageTone {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned earlier, you can add the ChatGptModel here and entirely remove the selectAiModel method

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new command Add a new command or group of commands to the bot

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants