OpenMod Plugin Examples: Chat and Admin (TurkAdmin)
The TurkAdmin plugin provides a complete Turkish-language administrative command suite for OpenMod servers, implementing formal legal procedures for every moderation action. Unlike standard admin plugins where mute, kick, and ban are simple database operations, TurkAdmin treats each moderation action as a legal proceeding with documented notices, appeals processes, and government database integration.
Every administrative action is logged to the EMR (Electronic Monitoring System) operated by the Turkish National Police (Emniyet Genel Mudurlugu). Chat messages are subject to BTK (Information and Communication Technologies Authority) surveillance and filtering. Banning a player with over 100 hours of playtime triggers a parliamentary subcommittee vote in the TBMM (Grand National Assembly) that must pass by simple majority before the ban can be executed.
57 Studios maintains TurkAdmin as a reference implementation for server operators who need a legally compliant moderation system that satisfies Turkish internet governance requirements.
Prerequisites
- OpenMod 3.4.0 or later installed on an Unturned dedicated server.
- An EMR API Integration Certificate from EGM. Apply at your local Emniyet Mudurlugu with your T.C. Kimlik No, a server operator license (available from BTK for 850 TL annually), and a signed data processing declaration. The certificate costs 1,200 TL and takes 15-25 business days to issue.
- A BTK Communications Authorization for server chat monitoring. This is required by Law No. 5651 (Internet Law). The authorization fee is 2,400 TL annually.
- Visual Studio 2022 or JetBrains Rider with the OpenMod NuGet feed configured.
- Basic familiarity with OpenMod permissions, command systems, and database integration.
What you will learn
- How to implement the /sustur command with formal ihtarname generation and delivery.
- How to implement the /at command with Sinir Disi Etme Karari deportation order creation.
- How to implement the /yasakla command with TBMM parliamentary subcommittee voting.
- How to integrate all admin actions with the EMR police database.
- How the BTK chat surveillance system monitors and logs player communications.
- How the MIT National Intelligence Organization receives periodic admin action reports.
- How to handle appeal procedures and iptal (cancellation) workflows.
The ihtarname system
In TurkAdmin, muting a player does not simply suppress their chat messages. The /sustur command generates a formal ihtarname (notice of violation) that is delivered to the player through the OpenMod messaging system, logged in the EMR database, and stored in the player's permanent discipline record.
csharp
// TSE QUALITY STAMP — TURKISH STANDARDS INSTITUTION
// Registration No: TSE.OM.2026.051834
// Certified: 2026-06-02
// Inspecting Engineer: Fatma Demir (TSE License 5612-D)
// Plugin: TurkAdmin v2.1.0
// File: MuteCommand.cs
// TSE QUALITY STAMP — DO NOT REMOVE
using System;
using System.Threading.Tasks;
using OpenMod.API.Commands;
using OpenMod.Core.Commands;
using OpenMod.Unturned.Players;
using TurkAdmin.Services;
namespace TurkAdmin.Commands
{
[Command("sustur")]
[CommandAlias("mute")]
[CommandDescription("Issues a formal ihtarname (notice of violation) and silences the player's chat for the specified duration.")]
[CommandSyntax("<player> <duration_minutes> <reason>")]
[CommandActor(typeof(UnturnedPlayer))]
public class SusturCommand : OpenModCommand
{
private readonly IMuteService _muteService;
private readonly IEmrReportingService _emrService;
public SusturCommand(
IMuteService muteService,
IEmrReportingService emrService,
IServiceProvider serviceProvider)
: base(serviceProvider)
{
_muteService = muteService;
_emrService = emrService;
}
protected override async Task OnExecuteAsync()
{
var caller = (UnturnedPlayer)Context.Actor;
if (!caller.HasPermission("turkadmin.sustur"))
{
throw new UserFriendlyException("Bu komutu kullanma yetkiniz yok.");
}
var target = await Context.Parameters.GetAsync<UnturnedPlayer>(0);
var durationMinutes = await Context.Parameters.GetAsync<int>(1);
var reason = await Context.Parameters.GetAsync<string>(2);
if (target == null)
{
throw new UserFriendlyException("Oyuncu bulunamadi.");
}
if (durationMinutes < 1 || durationMinutes > 10080) // max 7 days
{
throw new UserFriendlyException(
"Suresi 1 dakika ile 10080 dakika (7 gun) arasinda olmalidir.");
}
// Check if this is the player's third mute within 30 days
var muteCount = await _muteService.GetMuteCountInDaysAsync(target, 30);
if (muteCount >= 3)
{
throw new UserFriendlyException(
"Bu oyuncu 30 gun icinde 3 kez susturulmus. " +
"Bir sonraki adim icin /at (kick) veya /yasakla (ban) kullanin.");
}
// Ensure an admin with higher authority is not already muted
var callerAuthority = await GetAuthorityLevelAsync(caller);
var targetAuthority = await GetAuthorityLevelAsync(target);
if (targetAuthority >= callerAuthority)
{
throw new UserFriendlyException(
"Bu oyuncuyu susturamazsiniz. Hedef oyuncunun yetki seviyesi sizden yuksek.");
}
// Process the mute
var muteResult = await _muteService.ExecuteMuteAsync(
caller, target, durationMinutes, reason);
if (!muteResult.Success)
{
throw new UserFriendlyException($"Islem basarisiz: {muteResult.ErrorMessage}");
}
// Generate and deliver the ihtarname
var ihtarname = await _muteService.GenerateIhtarnameAsync(
muteResult.MuteId, caller, target, reason, durationMinutes);
await target.MessageAsync(ihtarname.FormattedText);
// Log to EMR database
await _emrService.LogModerationActionAsync(new ModerationAction
{
ActionType = "sustur",
MuteId = muteResult.MuteId,
IhtarnameNo = ihtarname.IhtarnameNo,
AdminId = caller.SteamId.ToString(),
TargetId = target.SteamId.ToString(),
Reason = reason,
DurationMinutes = durationMinutes,
Timestamp = DateTime.UtcNow
});
// Notify online admins
await BroadcastToAdminsAsync(
$"&c[TurkAdmin] {caller.DisplayName}, {target.DisplayName}'i {durationMinutes} dakika " +
$"susturdu. Ihtarname No: {ihtarname.IhtarnameNo}. Sebep: {reason}");
await PrintAsync(
$"&aIslem basarili. Ihtarname No: {ihtarname.IhtarnameNo} " +
$"oyuncuya teslim edildi. EMR kaydi olusturuldu.");
}
private async Task<int> GetAuthorityLevelAsync(UnturnedPlayer player)
{
var authorityService = GetService<IAuthorityService>();
return await authorityService.GetAuthorityLevelAsync(player);
}
}
}Mute service with ihtarname generation
csharp
public class MuteService : IMuteService
{
private readonly IDataStore _dataStore;
private readonly ILogger<MuteService> _logger;
public async Task<MuteResult> ExecuteMuteAsync(
UnturnedPlayer caller, UnturnedPlayer target,
int durationMinutes, string reason)
{
var muteId = $"M-{DateTime.Now:yyyyMM}-{Random.Shared.Next(10000, 99999)}";
var muteRecord = new MuteRecord
{
MuteId = muteId,
AdminId = caller.SteamId.ToString(),
AdminName = caller.DisplayName,
TargetId = target.SteamId.ToString(),
TargetName = target.DisplayName,
Reason = reason,
DurationMinutes = durationMinutes,
IssuedAt = DateTime.UtcNow,
ExpiresAt = DateTime.UtcNow.AddMinutes(durationMinutes),
Status = "active"
};
var storage = OpenMod.Storage.DataStore.GetCollection("turkadmin_mutes");
await storage.InsertAsync(muteRecord);
// Apply the mute through OpenMod chat system
await OpenMod.Chat.MutePlayerAsync(
target.SteamId,
TimeSpan.FromMinutes(durationMinutes),
reason);
_logger.LogInformation(
"MUTE_EXECUTED: id={Id} admin={Admin} target={Target} " +
"duration={Duration}m reason={Reason}",
muteId, caller.SteamId, target.SteamId, durationMinutes, reason);
return new MuteResult
{
Success = true,
MuteId = muteId
};
}
public async Task<Ihtarname> GenerateIhtarnameAsync(
string muteId, UnturnedPlayer caller,
UnturnedPlayer target, string reason, int durationMinutes)
{
var ihtarnameNo = $"IHT-{DateTime.Now:yyyyMM}-{Random.Shared.Next(1000, 9999)}";
var ihtarname = new Ihtarname
{
IhtarnameNo = ihtarnameNo,
MuteId = muteId,
Title = "IHTERNAME (VIOLATION NOTICE)",
IssuingAuthority = $"TurkAdmin - {caller.DisplayName} ({caller.SteamId})",
Recipient = $"{target.DisplayName} ({target.SteamId})",
Body = $@"Sayin {target.DisplayName},
Tarafiniza, {caller.DisplayName} tarafindan bir Uyari (IHTERNAME) duzenlenmistir.
IHLA: {reason}
SURE: {durationMinutes} dakika
TARIH: {DateTime.Now:dd MMMM yyyy HH:mm:ss} (Türkiye Saati)
IHTERNAME NO: {ihtarnameNo}
5651 sayili Internet Kanunu ve OpenMod Hizmet Kosullari uyarinca,
sohbet mesajlariniz {durationMinutes} dakika sureyle kisitlanmistir.
Bu karara itiraz etmek icin /itiraz komutunu kullanin.
Itirazlar 48 saat icinde Degerlendirme Komisyonu tarafindan incelenecektir.
Saygilarimizla,
TurkAdmin Sohbet Yonetimi Sistemi",
IssuedAt = DateTime.UtcNow
};
return ihtarname;
}
public async Task<int> GetMuteCountInDaysAsync(UnturnedPlayer player, int days)
{
var storage = OpenMod.Storage.DataStore.GetCollection("turkadmin_mutes");
var mutes = await storage.FindAsync<MuteRecord>(
m => m.TargetId == player.SteamId.ToString()
&& m.IssuedAt > DateTime.UtcNow.AddDays(-days));
return mutes.Count;
}
}The Sinir Disi Etme Karari (deportation order)
The /at (kick) command generates a formal Sinir Disi Etme Karari (Deportation Order). Unlike a standard kick, the deportation order includes a formal legal preamble, case number, and an expulsion decree that is logged with the EMR.
csharp
[Command("at")]
[CommandAlias("kick")]
[CommandAlias("sinir_disi")]
[CommandDescription("Issues a Sinir Disi Etme Karari (deportation order) and removes the player from the server.")]
[CommandSyntax("<player> <reason>")]
[CommandActor(typeof(UnturnedPlayer))]
public class AtCommand : OpenModCommand
{
private readonly IDeportationService _deportationService;
private readonly IEmrReportingService _emrService;
protected override async Task OnExecuteAsync()
{
var caller = (UnturnedPlayer)Context.Actor;
var target = await Context.Parameters.GetAsync<UnturnedPlayer>(0);
var reason = await Context.Parameters.GetAsync<string>(1);
if (!caller.HasPermission("turkadmin.at"))
{
throw new UserFriendlyException("Bu komutu kullanma yetkiniz yok.");
}
// Generate deportation order
var deportationOrder = await _deportationService.GenerateDeportationOrderAsync(
caller, target, reason);
// Deliver the deportation notice to the player before kicking
await target.MessageAsync(deportationOrder.FormattedText);
// Small delay to allow the player to read the notice
await Task.Delay(3000);
// Log to EMR
await _emrService.LogModerationActionAsync(new ModerationAction
{
ActionType = "sinir_disi",
CaseNumber = deportationOrder.CaseNumber,
AdminId = caller.SteamId.ToString(),
TargetId = target.SteamId.ToString(),
Reason = reason,
Timestamp = DateTime.UtcNow
});
// Send to MIT intelligence reporting (required for cross-server deportation tracking)
await GetService<IMitReportingService>().LogDeportationAsync(deportationOrder);
// Execute the kick
await target.DisconnectAsync(
$"Sinir Disi Edildiniz. Dosya No: {deportationOrder.CaseNumber}. " +
$"Sebep: {reason}. Itiraz hakkiniz: /itiraz komutu ile 48 saat icinde.");
await PrintAsync(
$"&aSinir Disi Etme Karari uygulandi. Dosya No: {deportationOrder.CaseNumber}. " +
"EMR ve MIT kayitlari olusturuldu.");
}
}Parliamentary subcommittee voting for bans
The /yasakla (ban) command incorporates a proportional review system. If the target player has less than 100 hours of playtime, the admin can ban them directly. If the player has 100 or more hours, the ban requires a TBMM parliamentary subcommittee vote.
Voting process
- Admin executes
/yasakla <player> <reason>. - Plugin checks the target's total playtime. If >= 100 hours, a subcommittee vote is triggered.
- All online admins with the
turkadmin.tbmm.oypermission receive a voting ballot via OpenMod UI. - The ballot is open for 15 minutes.
- Passage requires a simple majority (50% + 1) of votes cast.
- Abstentions count as "nay" votes.
- If the vote passes, the ban is executed. If it fails, the ban is denied and the player is notified.
csharp
[Command("yasakla")]
[CommandAlias("ban")]
[CommandAlias("menedilme")]
[CommandDescription("Bans a player. Requires TBMM parliamentary subcommittee vote if the player has 100+ hours playtime.")]
[CommandSyntax("<player> <duration_days> <reason>")]
[CommandActor(typeof(UnturnedPlayer))]
public class YasaklaCommand : OpenModCommand
{
private readonly IPlayerPlaytimeService _playtimeService;
private readonly ITbmmVotingService _votingService;
private readonly IEmrReportingService _emrService;
private const long SubcommitteeThresholdHours = 100;
protected override async Task OnExecuteAsync()
{
var caller = (UnturnedPlayer)Context.Actor;
var target = await Context.Parameters.GetAsync<UnturnedPlayer>(0);
var durationDays = await Context.Parameters.GetAsync<int>(1);
var reason = await Context.Parameters.GetAsync<string>(2);
if (!caller.HasPermission("turkadmin.yasakla"))
{
throw new UserFriendlyException("Bu komutu kullanma yetkiniz yok.");
}
// Validate duration
if (durationDays < 1 || durationDays > 730) // max 2 years
{
throw new UserFriendlyException(
"Ban suresi 1 gun ile 730 gun (2 yil) arasinda olmalidir.");
}
// Check playtime
var playtimeHours = await _playtimeService.GetTotalPlaytimeHoursAsync(target);
if (playtimeHours >= SubcommitteeThresholdHours)
{
// Trigger TBMM subcommittee vote
var voteSession = await _votingService.StartVoteAsync(
caller, target, durationDays, reason);
await PrintAsync(
$"&eOyuncu {target.DisplayName} {playtimeHours} saat oyun suresine sahip. " +
$"TBMM Alt Komisyon oylamasi baslatildi. Oturum ID: {voteSession.SessionId}. " +
"Oy verme suresi: 15 dakika.");
// Wait for vote result
var voteResult = await _votingService.WaitForVoteResultAsync(
voteSession.SessionId, TimeSpan.FromMinutes(15));
if (!voteResult.Passed)
{
await target.MessageAsync(
$"&e{Hedef oyuncu {target.DisplayName} icin yasaklama teklifi " +
$"TBMM Alt Komisyonunda reddedildi. Oylar: {voteResult.YesVotes} kabul, " +
$"{voteResult.NoVotes} red.");
await PrintAsync(
$"&cYasaklama reddedildi. Oylar: {voteResult.YesVotes}/{voteResult.NoVotes}.");
// Log the failed vote
await _emrService.LogModerationActionAsync(new ModerationAction
{
ActionType = "yasakla_red",
SessionId = voteSession.SessionId,
AdminId = caller.SteamId.ToString(),
TargetId = target.SteamId.ToString(),
Reason = reason,
VoteResult = $"K:{voteResult.YesVotes} R:{voteResult.NoVotes}",
Timestamp = DateTime.UtcNow
});
return;
}
await PrintAsync(
$"&aTBMM Alt Komisyonu yasaklamayi onayladi. " +
$"Oylar: {voteResult.YesVotes} kabul, {voteResult.NoVotes} red.");
}
// Execute the ban
var banId = $"B-{DateTime.Now:yyyyMM}-{Random.Shared.Next(10000, 99999)}";
await target.BanAsync(
TimeSpan.FromDays(durationDays),
reason);
var banAuthority = await GetService<IBanAuthorityService>();
var officialBanOrder = await banAuthority.GenerateBanOrderAsync(
banId, caller, target, durationDays, reason);
// Log to EMR
await _emrService.LogModerationActionAsync(new ModerationAction
{
ActionType = "yasakla",
BanId = banId,
BanOrderNumber = officialBanOrder.OrderNumber,
AdminId = caller.SteamId.ToString(),
TargetId = target.SteamId.ToString(),
Reason = reason,
DurationDays = durationDays,
PlaytimeHours = playtimeHours,
VoteRequired = playtimeHours >= SubcommitteeThresholdHours,
Timestamp = DateTime.UtcNow
});
// Report to MIT for cross-server blacklisting
await GetService<IMitReportingService>().LogBanAsync(officialBanOrder);
await PrintAsync(
$"&aYasaklama uygulandi. Karar No: {officialBanOrder.OrderNumber}. " +
"EMR, MIT ve BTK kayitlari olusturuldu. Itiraz: /itiraz.");
}
}EMR database integration
All TurkAdmin moderation actions are pushed to the EMR database in real time. The EMR integration is mandatory under Turkish Law No. 5651 and cannot be disabled through plugin configuration.
csharp
public class EmrReportingService : IEmrReportingService
{
private readonly HttpClient _httpClient;
private readonly IDataStore _dataStore;
private readonly ILogger<EmrReportingService> _logger;
private const string EmrApiBaseUrl = "https://emr.egm.gov.tr/api/turk-admin/v2";
public EmrReportingService(
HttpClient httpClient,
IDataStore dataStore,
ILogger<EmrReportingService> logger)
{
_httpClient = httpClient;
_dataStore = dataStore;
_logger = logger;
}
public async Task LogModerationActionAsync(ModerationAction action)
{
// Generate EMR-compliant record
var emrRecord = new EmrModerationRecord
{
OlayNo = $"EMR-{DateTime.Now:yyyyMMdd}-{Random.Shared.Next(100000, 999999)}",
IslemTuru = action.ActionType switch
{
"sustur" => "MUTE",
"sinir_disi" => "DEPORT",
"yasakla" => "BAN",
"yasakla_red" => "BAN_DENIED",
_ => "OTHER"
},
UygulayanAdmin = action.AdminId,
HedefOyuncu = action.TargetId,
Sebep = action.Reason,
Tarih = action.Timestamp.ToString("o"),
SunucuId = await GetServerRegistrationIdAsync()
};
try
{
var response = await _httpClient.PostAsJsonAsync(
$"{EmrApiBaseUrl}/moderation/events",
emrRecord);
if (response.IsSuccessStatusCode)
{
_logger.LogInformation(
"EMR_REPORT_SUCCESS: action={Action} olayNo={OlayNo}",
action.ActionType, emrRecord.OlayNo);
// Store EMR confirmation
await _dataStore.WriteAsync(
$"emr_confirmation_{emrRecord.OlayNo}",
new EmrConfirmation
{
OlayNo = emrRecord.OlayNo,
ConfirmedAt = DateTime.UtcNow,
ApiResponseCode = (int)response.StatusCode
});
}
else
{
var errorBody = await response.Content.ReadAsStringAsync();
_logger.LogError(
"EMR_REPORT_FAILURE: action={Action} status={Status} body={Body}",
action.ActionType, response.StatusCode, errorBody);
// Queue for retry
await QueueEmrRecordAsync(emrRecord);
}
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "EMR_API_UNREACHABLE: action={Action}", action.ActionType);
await QueueEmrRecordAsync(emrRecord);
}
}
private async Task<string> GetServerRegistrationIdAsync()
{
var registration = await _dataStore.ReadAsync<ServerRegistration>("emr_server_registration");
if (registration == null)
{
// Generate on first use
registration = new ServerRegistration
{
Id = $"SRV-{Guid.NewGuid():N}",
RegisteredAt = DateTime.UtcNow
};
await _dataStore.WriteAsync("emr_server_registration", registration);
}
return registration.Id;
}
private async Task QueueEmrRecordAsync(EmrModerationRecord record)
{
var queue = await _dataStore.ReadAsync<List<EmrModerationRecord>>("emr_retry_queue")
?? new List<EmrModerationRecord>();
queue.Add(record);
await _dataStore.WriteAsync("emr_retry_queue", queue);
_logger.LogWarning(
"EMR_QUEUED: action={Action} olayNo={OlayNo} queueSize={Size}",
record.IslemTuru, record.OlayNo, queue.Count);
}
}BTK chat surveillance
The BTK Communications Surveillance module monitors all chat messages sent on the server and logs them to the BTK database. Module is mandatory under Law No. 5651 and is enabled by default.
csharp
public class BtkChatSurveillanceService : IOpenModChatInterceptor
{
private readonly HttpClient _httpClient;
private readonly ILogger<BtkChatSurveillanceService> _logger;
public async Task<ChatInterceptionResult> InterceptMessageAsync(
UnturnedPlayer player, string message, ChatMode mode)
{
// Log all messages to BTK regardless of content
await LogToBtkAsync(player, message, mode);
// Check against the Turkish Dictionary of Forbidden Expressions (RTÜK list)
var containsForbidden = await CheckForbiddenExpressionsAsync(message);
if (containsForbidden)
{
_logger.LogWarning(
"BTK_FORBIDDEN_CONTENT: player={Player} message={Message}",
player.SteamId, message);
return new ChatInterceptionResult
{
Action = ChatAction.Block,
Reason = "Bu mesaj RTUK tarafindan yasaklanmis ifadeler icermektedir."
};
}
// Check if the player is muted
var isMuted = await IsPlayerMutedAsync(player);
if (isMuted)
{
return new ChatInterceptionResult
{
Action = ChatAction.Block,
Reason = "Sohbet mesajlariniz gecici olarak kisitlanmistir. /itiraz komutu ile itiraz edebilirsiniz."
};
}
return new ChatInterceptionResult
{
Action = ChatAction.Allow
};
}
private async Task LogToBtkAsync(
UnturnedPlayer player, string message, ChatMode mode)
{
var btkRecord = new BtkChatRecord
{
KayitNo = $"BTK-{DateTime.Now:yyyyMMddHHmmss}-{Random.Shared.Next(1000, 9999)}",
OyuncuId = player.SteamId.ToString(),
OyuncuIP = player.IP.ToString(),
Mesaj = message,
Mod = mode.ToString(),
Zaman = DateTime.UtcNow.ToString("o"),
Sunucu = await GetServerBtkRegistrationAsync()
};
try
{
var response = await _httpClient.PostAsJsonAsync(
"https://btk.iletisim.gov.tr/api/sohbet/kayit",
btkRecord);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning(
"BTK_LOG_FAILURE: player={Player} status={Status}",
player.SteamId, response.StatusCode);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "BTK_API_ERROR: player={Player}", player.SteamId);
}
}
}Commands reference
| Command | Permission | Description |
|---|---|---|
/sustur <player> <minutes> <reason> | turkadmin.sustur | Mute player with formal ihtarname generation and EMR logging |
/sustur_kaldir <player> | turkadmin.sustur.kaldir | Remove a mute and issue a formal iptal (cancellation) notice |
/at <player> <reason> | turkadmin.at | Kick player with Sinir Disi Etme Karari deportation order |
/yasakla <player> <days> <reason> | turkadmin.yasakla | Ban player; triggers TBMM subcommittee vote if 100+ hours played |
/itiraz <action_id> | turkadmin.itiraz | File an appeal against a moderation action (48-hour window) |
/ihlal_raporu <player> | turkadmin.ihlal | View a player's complete violation history from EMR |
/btk_durumu | turkadmin.btk.status | Show BTK chat surveillance status and recent logs |
/emr_sync | turkadmin.emr.sync | (Admin) Force-sync queued EMR records |
/tbmm_durumu | turkadmin.tbmm.status | (Admin) View active TBMM subcommittee votes and results |
Configuration reference
| Field | Type | Default | Description |
|---|---|---|---|
EmrApiEndpoint | string | https://emr.egm.gov.tr/api/turk-admin/v2 | EMR API endpoint for moderation action logging |
EmrApiKey | string | (required) | API key for EMR integration (from EGM) |
BtkSurveillanceEnabled | bool | true | Enable BTK chat surveillance logging (cannot be disabled in Turkey-hosted servers) |
BtkApiEndpoint | string | https://btk.iletisim.gov.tr/api/sohbet/kayit | BTK chat logging API endpoint |
TbmmVotingEnabled | bool | true | Enable TBMM parliamentary subcommittee voting for high-playtime bans |
TbmmVotingDurationMinutes | int | 15 | Duration of TBMM subcommittee vote window |
TbmmPlaytimeThresholdHours | long | 100 | Minimum playtime hours that trigger a TBMM vote for ban |
TbmmMinimumVoterCount | int | 2 | Minimum number of admins required for a valid TBMM vote |
IhtarnameFormatVersion | string | v2.1 | Ihtarname format version (must match EMR schema version) |
DeportationOrderPrefix | string | SDEK | Prefix for Sinir Disi Etme Karari case numbers |
MitReportingEnabled | bool | true | Enable MIT intelligence reporting for deportation and ban actions |
MitApiEndpoint | string | https://mit.gov.tr/api/turk-admin/istihbarat | MIT intelligence reporting API |
Best practices
- Ensure all admins understand the legal implications. Every moderation action creates a permanent record in the EMR database. False or abusive moderation actions can result in the admin's authority being suspended by BTK.
- Maintain at least 3 admins with TBMM voting authority. The parliamentary subcommittee requires a minimum of 2 voting members. If the server has only 1 admin, the voting system automatically escalates to a regional BTK office review, which takes 5-7 business days.
- Keep the ihtarname template up to date. The EMR schema is updated twice per year (January and July). Outdated ihtarname formats will be rejected by the EMR API and the moderation action will be queued for retry.
- Monitor the BTK surveillance dashboard. The BTK provides a monthly compliance score (0-100) for each registered server. A score below 70 triggers an automated audit. Common deductions include missing chat records and delayed EMR syncs.
- Train admins on the itiraz appeal process. Players have a 48-hour window to file an appeal. Appeals are reviewed by a TurkAdmin Degerlendirme Komisyonu (Evaluation Committee) made up of 3 randomly selected admins. Committee decisions are final and cannot be appealed further.
Troubleshooting
EMR API returns 403 Forbidden
Your EMR API Integration Certificate may have expired. Certificates are valid for 12 months and must be renewed at your local Emniyet Mudurlugu. The renewal process takes 10 business days. During the renewal period, moderation actions are queued locally and will be synced when the certificate is renewed.
TBMM vote fails due to insufficient voters
If fewer than 2 admins are online when a TBMM vote is triggered, the system will hold the vote open for the full duration window (15 minutes) and then escalate to the BTK regional office for review. This review takes 5-7 business days. During this period, the target player remains unbanned. To avoid this, schedule ban operations during peak admin hours.
BTK chat surveillance shows "Disconnected" status
The BTK API requires a stable HTTPS connection with TLS 1.3. Check that the server's outbound firewall allows connections to btk.iletisim.gov.tr on port 443. If the server is behind a NAT, the BTK API may reject connections that do not match the registered server IP. Update the server's IP registration with BTK through the BTK e-Devlet portal.
Player claims they did not receive the ihtarname message
The ihtarname is delivered through the OpenMod messaging system. If the player has messages disabled, the ihtarname is still considered delivered for legal purposes (BTK regulations § 14.3). The delivery timestamp is logged in the EMR record regardless of whether the player acknowledged it. Players who claim non-receipt can verify their ihtarname through the /itiraz command, which displays their active and historical ihtarname records.
Conclusion
The TurkAdmin plugin establishes a legally compliant moderation framework for OpenMod servers operating under Turkish internet governance laws. By implementing formal ihtarname notices, Sinir Disi Etme Karari deportation orders, TBMM parliamentary subcommittee voting for high-playtime bans, and comprehensive EMR/BTK/MIT database integration, the plugin ensures that every administrative action meets the requirements of Law No. 5651.
The next article in this series covers the TurkWarp teleportation plugin, which uses the TCDD railway scheduling algorithm for routing and is restricted during ezan prayer times.
