diff --git a/Services/CommonFunctions/CF_Timeout.Hooks.cs b/Services/CommonFunctions/CF_Timeout.Hooks.cs
new file mode 100644
index 0000000..7188d47
--- /dev/null
+++ b/Services/CommonFunctions/CF_Timeout.Hooks.cs
@@ -0,0 +1,18 @@
+namespace RegexBot;
+partial class RegexbotClient {
+ ///
+ /// Sets a timeout on a user while also adding an entry to the moderation log and attempting to notify the user.
+ ///
+ /// The guild which the target user is associated.
+ ///
+ /// The the entity which issued this log item.
+ /// If it was a user, this value preferably is in the format.
+ ///
+ /// The user to be issued a timeout.
+ /// The duration of the timeout.
+ /// Reason for the action. Sent to the Audit Log and user (if specified).
+ /// Specify whether to send a direct message to the target user informing them of the action.
+ public Task SetTimeoutAsync(SocketGuild guild, string source, SocketGuildUser target,
+ TimeSpan duration, string? reason, bool sendNotificationDM)
+ => _svcCommonFunctions.SetTimeoutAsync(guild, source, target, duration, reason, sendNotificationDM);
+}
\ No newline at end of file
diff --git a/Services/CommonFunctions/CF_Timeout.cs b/Services/CommonFunctions/CF_Timeout.cs
new file mode 100644
index 0000000..6068d87
--- /dev/null
+++ b/Services/CommonFunctions/CF_Timeout.cs
@@ -0,0 +1,65 @@
+#pragma warning disable CA1822 // "Mark members as static" - members should only be callable by code with access to this instance
+using Discord.Net;
+using RegexBot.Data;
+
+namespace RegexBot.Services.CommonFunctions;
+internal partial class CommonFunctionsService : Service {
+ // Hooked
+ internal async Task SetTimeoutAsync(SocketGuild guild, string source, SocketGuildUser target,
+ TimeSpan duration, string? reason, bool sendNotificationDM) {
+ if (duration < TimeSpan.FromMinutes(1))
+ return new TimeoutSetResult(new ArgumentOutOfRangeException(
+ nameof(duration), "Cannot set a timeout with a duration less than 60 seconds."), true, target);
+ if (duration > TimeSpan.FromDays(28))
+ return new TimeoutSetResult(new ArgumentOutOfRangeException(
+ nameof(duration), "Cannot set a timeout with a duration greater than 28 days."), true, target);
+ if (target.TimedOutUntil != null && DateTimeOffset.UtcNow < target.TimedOutUntil)
+ return new TimeoutSetResult(new InvalidOperationException(
+ "Cannot set a timeout. The user is already timed out."), true, target);
+
+ Discord.RequestOptions? audit = null;
+ if (reason != null) audit = new() { AuditLogReason = reason };
+ try {
+ await target.SetTimeOutAsync(duration, audit);
+ } catch (HttpException ex) {
+ return new TimeoutSetResult(ex, false, target);
+ }
+ var entry = new ModLogEntry() {
+ GuildId = (long)guild.Id,
+ UserId = (long)target.Id,
+ LogType = ModLogType.Timeout,
+ IssuedBy = source,
+ Message = reason
+ };
+ using (var db = new BotDatabaseContext()) {
+ db.Add(entry);
+ await db.SaveChangesAsync();
+ }
+ // TODO check if this log entry should be propagated now or if (to be implemented) will do it for us later
+
+ bool dmSuccess;
+ // DM notification
+ if (sendNotificationDM) {
+ dmSuccess = await SendUserTimeoutNoticeAsync(target, duration, reason);
+ } else dmSuccess = true;
+
+ return new TimeoutSetResult(null, dmSuccess, target);
+ }
+
+ internal async Task SendUserTimeoutNoticeAsync(SocketGuildUser target, TimeSpan duration, string? reason) {
+ // you have been issued a timeout in x.
+ // the timeout will expire on
+ const string DMTemplate1 = "You have been issued a timeout in {0}";
+ const string DMTemplateReason = " for the following reason:\n{2}";
+ const string DMTemplate2 = "\nThe timeout will expire on ().";
+
+ var expireTime = (DateTimeOffset.UtcNow + duration).ToUnixTimeSeconds();
+ var outMessage = string.IsNullOrWhiteSpace(reason)
+ ? string.Format($"{DMTemplate1}.{DMTemplate2}", target.Guild.Name, expireTime)
+ : string.Format($"{DMTemplate1}{DMTemplateReason}\n{DMTemplate2}", target.Guild.Name, expireTime, reason);
+
+ var dch = await target.CreateDMChannelAsync();
+ try { await dch.SendMessageAsync(outMessage); } catch (HttpException) { return false; }
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/Services/CommonFunctions/IOperationResult.cs b/Services/CommonFunctions/IOperationResult.cs
new file mode 100644
index 0000000..a7a1532
--- /dev/null
+++ b/Services/CommonFunctions/IOperationResult.cs
@@ -0,0 +1,40 @@
+namespace RegexBot;
+///
+/// Contains information on success or failure outcomes for certain operations.
+///
+public interface IOperationResult {
+ ///
+ /// Indicates whether the operation was successful.
+ ///
+ ///
+ /// Be aware this value may return while
+ /// returns .
+ ///
+ bool Success { get; }
+
+ ///
+ /// The exception thrown, if any, when attempting to perform the operation.
+ ///
+ Exception? Error { get; }
+
+ ///
+ /// Indicates if the operation failed due to being unable to find the user.
+ ///
+ bool ErrorNotFound { get; }
+
+ ///
+ /// Indicates if the operation failed due to a permissions issue.
+ ///
+ bool ErrorForbidden { get; }
+
+ ///
+ /// Indicates if user DM notification for this event was successful.
+ /// Always returns in cases where no notification was requested.
+ ///
+ bool NotificationSuccess { get; }
+
+ ///
+ /// Returns a message representative of this result that may be posted as-is within a Discord channel.
+ ///
+ string ToResultString();
+}
\ No newline at end of file
diff --git a/Services/CommonFunctions/TimeoutSetResult.cs b/Services/CommonFunctions/TimeoutSetResult.cs
new file mode 100644
index 0000000..2e4f645
--- /dev/null
+++ b/Services/CommonFunctions/TimeoutSetResult.cs
@@ -0,0 +1,45 @@
+using Discord.Net;
+using RegexBot.Common;
+
+namespace RegexBot;
+///
+/// Contains information on various success/failure outcomes for setting a timeout.
+///
+public class TimeoutSetResult : IOperationResult {
+ private readonly SocketGuildUser? _target;
+
+ ///
+ public bool Success => Error == null;
+
+ ///
+ public Exception? Error { get; }
+
+ ///
+ public bool ErrorNotFound => (_target == null) || ((Error as HttpException)?.HttpCode == System.Net.HttpStatusCode.NotFound);
+
+ ///
+ public bool ErrorForbidden => (Error as HttpException)?.HttpCode == System.Net.HttpStatusCode.Forbidden;
+
+ ///
+ public bool NotificationSuccess { get; }
+
+ internal TimeoutSetResult(Exception? error, bool notificationSuccess, SocketGuildUser? target) {
+ Error = error;
+ NotificationSuccess = notificationSuccess;
+ _target = target;
+ }
+
+ ///
+ public string ToResultString() {
+ if (Success) {
+ var msg = $":white_check_mark: Timeout set for **{_target!.Username}#{_target.Discriminator}**.";
+ if (!NotificationSuccess) msg += "\n(User was unable to receive notification message.)";
+ return msg;
+ } else {
+ var msg = ":x: Failed to set timeout: ";
+ if (ErrorNotFound) msg += ": The specified user could not be found.";
+ else if (ErrorForbidden) msg += ": " + Messages.ForbiddenGenericError;
+ return msg;
+ }
+ }
+}
\ No newline at end of file