program DiscordCreateThread; {$MODE OBJFPC} {$H+} uses SysUtils, Classes, fphttpclient, opensslsockets, DateUtils; const DISCORD_API = 'https://discord.com/api/v10'; function GetEnvDef(const Name, Def: string): string; begin if GetEnvironmentVariable(Name) = '' then GetEnvDef := Def else GetEnvDef := GetEnvironmentVariable(Name); end; function GetArchiveDuration: Integer; begin GetArchiveDuration := StrToIntDef(GetEnvironmentVariable('ARCHIVE_DURATION'), 10080); end; function GetThreadName: string; var Base: string; begin Base := GetEnvironmentVariable('THREAD_NAME'); GetThreadName := Base + ' (' + FormatDateTime('yyyy-mm-dd', Now) + ')'; end; function HttpPost(const URL, Body, Token: string): string; var Client: TFPHttpClient; BodyStream, ResponseStream: TStringStream; begin Client := TFPHttpClient.Create(nil); BodyStream := TStringStream.Create(Body); ResponseStream := TStringStream.Create(''); try Client.AddHeader('Authorization', 'Bot ' + Token); Client.AddHeader('Content-Type', 'application/json'); Client.RequestBody := BodyStream; Client.Post(URL, ResponseStream); HttpPost := ResponseStream.DataString; finally ResponseStream.Free; BodyStream.Free; Client.Free; end; end; procedure RunCore; var Token, ChannelID, ThreadMessage: string; ThreadBody, MessageBody, Response: string; ThreadIDStart, ThreadIDEnd: Integer; ThreadID: string; begin Token := GetEnvironmentVariable('DISCORD_BOT_TOKEN'); ChannelID := GetEnvironmentVariable('DISCORD_CHANNEL_ID'); ThreadMessage := GetEnvironmentVariable('THREAD_MESSAGE'); ThreadBody := Format( '{"name":"%s","auto_archive_duration":%d}', [StringReplace(GetThreadName, '"', '\"', [rfReplaceAll]), GetArchiveDuration] ); Response := HttpPost(DISCORD_API + '/channels/' + ChannelID + '/threads', ThreadBody, Token); ThreadIDStart := Pos('"id":"', Response); if ThreadIDStart > 0 then begin Inc(ThreadIDStart, 6); ThreadIDEnd := Pos('"', Response, ThreadIDStart); ThreadID := Copy(Response, ThreadIDStart, ThreadIDEnd - ThreadIDStart); if ThreadID <> '' then begin MessageBody := Format('{"content":"%s"}', [StringReplace(ThreadMessage, '"', '\"', [rfReplaceAll])]); HttpPost(DISCORD_API + '/channels/' + ThreadID + '/messages', MessageBody, Token); end; end; end; // RUN_AT format: "::" // day_of_week: 1=Sunday, 2=Monday, 3=Tuesday, 4=Wednesday, 5=Thursday, 6=Friday, 7=Saturday function ParseRunAt(out TargetDow, TargetHour, TargetMinute: Integer): Boolean; var RunAt: string; Parts: TStringList; begin ParseRunAt := False; RunAt := GetEnvironmentVariable('RUN_AT'); if RunAt = '' then Exit; Parts := TStringList.Create; try Parts.Delimiter := ':'; Parts.StrictDelimiter := True; Parts.DelimitedText := RunAt; if Parts.Count < 3 then Exit; TargetDow := StrToIntDef(Parts[0], -1); TargetHour := StrToIntDef(Parts[1], -1); TargetMinute := StrToIntDef(Parts[2], -1); if (TargetDow < 1) or (TargetDow > 7) then Exit; if (TargetHour < 0) or (TargetHour > 23) then Exit; if (TargetMinute < 0) or (TargetMinute > 59) then Exit; ParseRunAt := True; finally Parts.Free; end; end; function ShouldFire(TargetDow, TargetHour, TargetMinute: Integer): Boolean; var H, M, S, MS: Word; begin DecodeTime(SysUtils.Now, H, M, S, MS); ShouldFire := (DayOfWeek(SysUtils.Now) = TargetDow) and (Integer(H) = TargetHour) and (Integer(M) = TargetMinute); end; const DAY_NAMES: array[1..7] of string = ( 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' ); var Fired: Boolean; LastCheckedMinute: Integer; CurrentMinute: Integer; H, M, S, MS: Word; TargetDow, TargetHour, TargetMinute: Integer; begin if not ParseRunAt(TargetDow, TargetHour, TargetMinute) then begin WriteLn('Error: RUN_AT environment variable is not set or invalid.'); WriteLn('Format: RUN_AT=::'); WriteLn('Example (Tuesday 9:00): RUN_AT=3:9:0'); Halt(1); end; WriteLn('==========================================='); WriteLn(' Discord Thread Scheduler'); WriteLn('==========================================='); WriteLn(Format(' Started at : %s', [FormatDateTime('yyyy-mm-dd hh:nn:ss', SysUtils.Now)])); WriteLn(Format(' Channel ID : %s', [GetEnvironmentVariable('DISCORD_CHANNEL_ID')])); WriteLn(Format(' Thread name: %s', [GetEnvironmentVariable('THREAD_NAME')])); WriteLn(Format(' Fires every: %s at %02d:%02d', [DAY_NAMES[TargetDow], TargetHour, TargetMinute])); WriteLn('==========================================='); Fired := False; LastCheckedMinute := -1; while True do begin DecodeTime(SysUtils.Now, H, M, S, MS); CurrentMinute := Integer(H) * 60 + Integer(M); if CurrentMinute <> LastCheckedMinute then begin LastCheckedMinute := CurrentMinute; if ShouldFire(TargetDow, TargetHour, TargetMinute) then begin if not Fired then begin WriteLn(Format('[%s] Trigger matched, running core...', [FormatDateTime('yyyy-mm-dd hh:nn', SysUtils.Now)])); try RunCore; WriteLn('Core executed successfully.'); except on E: Exception do WriteLn('Error: ', E.Message); end; Fired := True; end; end else Fired := False; end; Sleep(10000); end; end.