public override async Task OnCommand(CommandArguments command) { var s = command.Message.Split(' '); if (s.Length < 2) { command.Reply($"Usage:{Colors.OLIVE} queue <app/sub> <id>"); return; } if (!uint.TryParse(s[1], out var id)) { command.Reply("Your ID does not look like a number."); return; } switch (s[0]) { case "app": var appName = Steam.GetAppName(id); if (!string.IsNullOrEmpty(appName)) { await StoreQueue.AddAppToQueue(id); command.Reply($"App {Colors.BLUE}{id}{Colors.NORMAL} ({appName}) has been added to the store update queue."); return; } command.Reply("This app is not in the database."); return; case "package": case "sub": if (id == 0) { command.Reply("Sub 0 can not be queued."); return; } var subName = Steam.GetPackageName(id); if (!string.IsNullOrEmpty(subName)) { await StoreQueue.AddPackageToQueue(id); command.Reply($"Package {Colors.BLUE}{id}{Colors.NORMAL} ({subName}) has been added to the store update queue."); return; } command.Reply("This package is not in the database."); return; default: command.Reply("You can only queue apps and packages."); return; } }
public static void HandleToken(uint id, ulong accessToken) { if (!SecretTokens.ContainsKey(id)) { SecretTokens.Add(id, accessToken); IRC.Instance.SendOps($"{Colors.GREEN}[Tokens]{Colors.NORMAL} Added a new token that the bot got itself:{Colors.BLUE} {id} {Colors.NORMAL}({Steam.GetAppName(id)})"); Log.WriteInfo("PICSTokens", "New token for appid {0}", id); using (var db = Database.Get()) { db.Execute("INSERT INTO `PICSTokens` (`AppID`, `Token`) VALUES(@AppID, @Token)", new { AppID = id, Token = accessToken } ); } } else if (SecretTokens[id] != accessToken) { IRC.Instance.SendOps($"{Colors.GREEN}[Tokens]{Colors.NORMAL} Bot got a token that mismatches the one in database: {SecretTokens[id]} != {accessToken}"); } }
public void OnMessage(CommandArguments command) { var matches = SteamLinkMatch.Matches(command.Message); foreach (Match match in matches) { var page = match.Groups["page"].Value; // Ignore sub pages, easier to do it here rather than in regex if (!string.IsNullOrEmpty(page)) { continue; } var appType = string.Empty; var id = uint.Parse(match.Groups["id"].Value); var isPackage = match.Groups["type"].Value == "sub"; string name; if (isPackage) { name = Steam.GetPackageName(id); } else { App data; using (var db = Database.Get()) { data = db.Query <App>("SELECT `AppID`, `Apps`.`Name`, `LastKnownName`, `AppsTypes`.`DisplayName` as `AppTypeString` FROM `Apps` JOIN `AppsTypes` ON `Apps`.`AppType` = `AppsTypes`.`AppType` WHERE `AppID` = @AppID", new { AppID = id }).SingleOrDefault(); } if (data.AppID == 0) { continue; } name = string.IsNullOrEmpty(data.LastKnownName) ? data.Name : data.LastKnownName; name = Utils.RemoveControlCharacters(name); appType = data.AppTypeString; } if (command.Message.IndexOf(name, StringComparison.CurrentCultureIgnoreCase) >= 0) { continue; } string priceInfo = isPackage ? string.Empty : GetFormattedPrices(id); if (command.CommandType == ECommandType.SteamChatRoom) { Steam.Instance.Friends.SendChatRoomMessage(command.ChatRoomID, EChatEntryType.ChatMsg, string.Format("» {0} {1} — {2}{3}", isPackage ? "Package" : "App", id, Colors.StripColors(name), priceInfo)); continue; } IRC.Instance.SendReply(command.Recipient, string.Format("{0}» {1}{2} {3} —{4} {5}{6}{7}", Colors.OLIVE, Colors.NORMAL, isPackage ? "Package" : appType, id, Colors.BLUE, name, Colors.LIGHTGRAY, priceInfo ), false ); } }
private async Task DownloadDepots(uint appID, List <ManifestJob> depots) { Log.WriteDebug("Depot Downloader", "Will process {0} depots ({1} depot locks left)", depots.Count, DepotLocks.Count); var processTasks = new List <Task <EResult> >(); var anyFilesDownloaded = false; var willDownloadFiles = false; foreach (var depot in depots) { if (depot.DepotKey == null) { await GetDepotDecryptionKey(Steam.Instance.Apps, depot, appID); if (depot.DepotKey == null) { RemoveLock(depot.DepotID); continue; } } depot.Server = GetContentServer(); DepotManifest depotManifest = null; var lastError = string.Empty; for (var i = 0; i <= 5; i++) { try { depotManifest = await CDNClient.DownloadManifestAsync(depot.DepotID, depot.ManifestID, depot.Server, string.Empty, depot.DepotKey); break; } catch (Exception e) { lastError = e.Message; Log.WriteError("Depot Processor", "Failed to download depot manifest for app {0} depot {1} ({2}: {3}) (#{4})", appID, depot.DepotID, depot.Server, lastError, i); } depot.Server = GetContentServer(); if (depotManifest == null) { await Task.Delay(Utils.ExponentionalBackoff(i)); } } if (depotManifest == null) { RemoveLock(depot.DepotID); if (FileDownloader.IsImportantDepot(depot.DepotID)) { IRC.Instance.SendOps("{0}[{1}]{2} Failed to download manifest ({3})", Colors.OLIVE, depot.DepotName, Colors.NORMAL, lastError); } if (!Settings.IsFullRun) { JobManager.AddJob(() => Steam.Instance.Apps.PICSGetAccessTokens(appID, null)); } continue; } var task = ProcessDepotAfterDownload(depot, depotManifest); processTasks.Add(task); if (!FileDownloader.IsImportantDepot(depot.DepotID)) { continue; } willDownloadFiles = true; task = TaskManager.Run(async() => { var result = EResult.Fail; try { result = await FileDownloader.DownloadFilesFromDepot(depot, depotManifest); if (result == EResult.OK) { anyFilesDownloaded = true; } } catch (Exception e) { ErrorReporter.Notify($"Depot Processor {depot.DepotID}", e); } return(result); }).Unwrap(); processTasks.Add(task); } if (SaveLocalConfig) { SaveLocalConfig = false; LocalConfig.Save(); } await Task.WhenAll(processTasks).ConfigureAwait(false); Log.WriteDebug("Depot Downloader", "{0} depot downloads finished", depots.Count); // TODO: use ContinueWith on tasks if (!anyFilesDownloaded && !willDownloadFiles) { foreach (var depot in depots) { RemoveLock(depot.DepotID); } return; } lock (UpdateScriptLock) { foreach (var depot in depots) { if (depot.Result == EResult.OK) { RunUpdateScript(UpdateScript, string.Format("{0} no-git", depot.DepotID)); } else if (depot.Result != EResult.Ignored) { Log.WriteWarn("Depot Processor", $"Download failed for {depot.DepotID}"); using (var db = Database.Get()) { // Mark this depot for redownload db.Execute("UPDATE `Depots` SET `LastManifestID` = 0 WHERE `DepotID` = @DepotID", new { depot.DepotID }); } } RemoveLock(depot.DepotID); } // Only commit changes if all depots downloaded if (processTasks.All(x => x.Result == EResult.OK || x.Result == EResult.Ignored)) { if (!RunUpdateScriptForApp(appID, depots[0].BuildID)) { RunUpdateScript(UpdateScript, "0"); } } else { Log.WriteDebug("Depot Processor", "Reprocessing the app {0} because some files failed to download", appID); IRC.Instance.SendOps("{0}[{1}]{2} Reprocessing the app due to download failures", Colors.OLIVE, Steam.GetAppName(appID), Colors.NORMAL ); JobManager.AddJob(() => Steam.Instance.Apps.PICSGetAccessTokens(appID, null)); } } }
public override async Task OnCommand(CommandArguments command) { if (command.Message.Length == 0) { command.Reply("Usage:{0} players <appid or partial game name>", Colors.OLIVE); command.Notice("Use {0}^{1} and {2}${3} just like in regex to narrow down your match, e.g:{4} !players Portal$", Colors.BLUE, Colors.NORMAL, Colors.BLUE, Colors.NORMAL, Colors.OLIVE); return; } uint appID; string name; if (!uint.TryParse(command.Message, out appID)) { name = command.Message; if (!Utils.ConvertUserInputToSQLSearch(ref name)) { command.Reply("Your request is invalid or too short."); return; } using (var db = Database.GetConnection()) { appID = db.ExecuteScalar <uint>("SELECT `AppID` FROM `Apps` LEFT JOIN `AppsTypes` ON `Apps`.`AppType` = `AppsTypes`.`AppType` WHERE (`AppsTypes`.`Name` IN ('game', 'application', 'video', 'hardware') AND (`Apps`.`StoreName` LIKE @Name OR `Apps`.`Name` LIKE @Name)) OR (`AppsTypes`.`Name` = 'unknown' AND `Apps`.`LastKnownName` LIKE @Name) ORDER BY `LastUpdated` DESC LIMIT 1", new { Name = name }); } if (appID == 0) { command.Reply("Nothing was found matching your request."); return; } } var callback = await Steam.Instance.UserStats.GetNumberOfCurrentPlayers(appID); if (appID == 0) { appID = 753; } string appType, type = "playing"; name = Steam.GetAppName(appID, out appType); if (callback.Result != EResult.OK) { command.Reply("Unable to request player count for {0}{1}{2}: {3}{4}", Colors.BLUE, name, Colors.NORMAL, Colors.RED, callback.Result); return; } switch (appType) { case "Tool": case "Config": case "Application": type = "using"; break; case "Legacy Media": case "Video": type = "watching"; break; case "Guide": type = "reading"; break; case "Hardware": type = "bricking"; break; } command.Reply( "People {0} {1}{2}{3} right now: {4}{5:N0}{6} -{7} {8}", type, Colors.BLUE, name, Colors.NORMAL, Colors.OLIVE, callback.NumPlayers, Colors.NORMAL, Colors.DARKBLUE, SteamDB.GetAppURL(appID, "graphs") ); }
private static void PrintImportants(SteamApps.PICSChangesCallback callback) { // Apps var important = callback.AppChanges.Keys.Intersect(Application.ImportantApps.Keys); foreach (var app in important) { var appName = Steam.GetAppName(app, out var appType); IRC.Instance.AnnounceImportantAppUpdate(app, "{0} update: {1}{2}{3} -{4} {5}", appType, Colors.BLUE, appName, Colors.NORMAL, Colors.DARKBLUE, SteamDB.GetAppURL(app, "history")); if (Settings.Current.CanQueryStore) { Steam.Instance.UnifiedMessages.SendMessage("ChatRoom.SendChatMessage#1", new CChatRoom_SendChatMessage_Request { chat_group_id = 1147, chat_id = 10208600, message = $"[Changelist {callback.CurrentChangeNumber}] {appType} update: {appName}\n{SteamDB.GetAppURL(app, "history")}?utm_source=Steam&utm_medium=Steam&utm_campaign=SteamDB%20Group%20Chat" }); } } // Packages important = callback.PackageChanges.Keys.Intersect(Application.ImportantSubs.Keys); foreach (var package in important) { IRC.Instance.AnnounceImportantPackageUpdate("Package update: {0}{1}{2} -{3} {4}", Colors.BLUE, Steam.GetPackageName(package), Colors.NORMAL, Colors.DARKBLUE, SteamDB.GetPackageURL(package, "history")); } }
private async Task SendChangelistsToIRC(SteamApps.PICSChangesCallback callback) { if (DateTime.Now > ChangelistBurstTime) { ChangelistBurstTime = DateTime.Now.AddMinutes(5); ChangelistBurstCount = 0; } // Group apps and package changes by changelist number var changelists = new Dictionary <uint, IrcChangelistGroup>(); foreach (var app in callback.AppChanges.Values) { if (!changelists.ContainsKey(app.ChangeNumber)) { changelists[app.ChangeNumber] = new IrcChangelistGroup(); } changelists[app.ChangeNumber].Apps.Add(app.ID); } foreach (var package in callback.PackageChanges.Values) { if (!changelists.ContainsKey(package.ChangeNumber)) { changelists[package.ChangeNumber] = new IrcChangelistGroup(); } changelists[package.ChangeNumber].Packages.Add(package.ID); } foreach (var(changeNumber, changeList) in changelists.OrderBy(x => x.Key)) { var appCount = changeList.Apps.Count; var packageCount = changeList.Packages.Count; var message = $"Changelist {Colors.BLUE}{changeNumber}{Colors.NORMAL} {Colors.DARKGRAY}({appCount:N0} apps and {packageCount:N0} packages)"; var changesCount = appCount + packageCount; if (changesCount >= 50) { IRC.Instance.SendMain($"Big {message}{Colors.DARKBLUE} {SteamDB.GetChangelistURL(changeNumber)}"); } if (ChangelistBurstCount++ >= CHANGELIST_BURST_MIN || changesCount > 300) { if (appCount > 0) { message += $" (Apps: {string.Join(", ", changeList.Apps)})"; } if (packageCount > 0) { message += $" (Packages: {string.Join(", ", changeList.Packages)})"; } IRC.Instance.SendAnnounce($"{Colors.RED}»{Colors.NORMAL} {message}"); continue; } IRC.Instance.SendAnnounce("{0}»{1} {2}", Colors.RED, Colors.NORMAL, message); if (appCount > 0) { Dictionary <uint, App> apps; using (var db = Database.Get()) { apps = (await db.QueryAsync <App>("SELECT `AppID`, `Name`, `LastKnownName` FROM `Apps` WHERE `AppID` IN @Ids", new { Ids = changeList.Apps })).ToDictionary(x => x.AppID, x => x); } foreach (var appId in changeList.Apps) { apps.TryGetValue(appId, out var data); IRC.Instance.SendAnnounce($" App: {Colors.BLUE}{appId}{Colors.NORMAL} - {Steam.FormatAppName(appId, data)}"); } } if (packageCount > 0) { Dictionary <uint, Package> packages; using (var db = Database.Get()) { packages = (await db.QueryAsync <Package>("SELECT `SubID`, `Name`, `LastKnownName` FROM `Subs` WHERE `SubID` IN @Ids", new { Ids = changeList.Packages })).ToDictionary(x => x.SubID, x => x); } foreach (var packageId in changeList.Packages) { packages.TryGetValue(packageId, out var data); IRC.Instance.SendAnnounce($" Package: {Colors.BLUE}{packageId}{Colors.NORMAL} - {Steam.FormatPackageName(packageId, data)}"); } } } }
private async Task <bool> ProcessKey(string keyName, string displayName, string value, KeyValue newKv = null) { if (keyName.Length > 90) { Log.WriteError("App Processor", "Key {0} for AppID {1} is too long, not inserting info.", keyName, AppID); return(false); } // All keys in PICS are supposed to be lower case keyName = keyName.ToLower().Trim(); if (!CurrentData.ContainsKey(keyName)) { var key = KeyNameCache.GetAppKeyID(keyName); if (key == 0) { var type = newKv != null ? 86 : 0; // 86 is a hardcoded const for the website key = await KeyNameCache.CreateAppKey(keyName, displayName, type); if (key == 0) { // We can't insert anything because key wasn't created Log.WriteError("App Processor", "Failed to create key {0} for AppID {1}, not inserting info.", keyName, AppID); return(false); } IRC.Instance.SendOps("New app keyname: {0}{1} {2}(ID: {3}) ({4}) - {5}", Colors.BLUE, keyName, Colors.LIGHTGRAY, key, displayName, SteamDB.GetAppURL(AppID, "history")); } await DbConnection.ExecuteAsync("INSERT INTO `AppsInfo` (`AppID`, `Key`, `Value`) VALUES (@AppID, @Key, @Value)", new { AppID, Key = key, Value = value }); await MakeHistory("created_key", key, string.Empty, value); if ((keyName == "extended_developer" || keyName == "extended_publisher") && value == "Valve") { IRC.Instance.SendOps("New {0}=Valve app: {1}{2}{3} -{4} {5}", displayName, Colors.BLUE, Steam.GetAppName(AppID), Colors.NORMAL, Colors.DARKBLUE, SteamDB.GetAppURL(AppID, "history")); } if (keyName == "common_oslist" && value.Contains("linux")) { PrintLinux(); } return(true); } var data = CurrentData[keyName]; if (data.Processed) { Log.WriteWarn("App Processor", "Duplicate key {0} in AppID {1}", keyName, AppID); return(false); } data.Processed = true; CurrentData[keyName] = data; if (data.Value.Equals(value)) { return(false); } await DbConnection.ExecuteAsync("UPDATE `AppsInfo` SET `Value` = @Value WHERE `AppID` = @AppID AND `Key` = @Key", new { AppID, data.Key, Value = value }); if (newKv != null) { await MakeHistoryForJson(data.Key, data.Value, value, newKv); } else { await MakeHistory("modified_key", data.Key, data.Value, value); } if (keyName == "common_oslist" && value.Contains("linux") && !data.Value.Contains("linux")) { PrintLinux(); } return(true); }
public static void HandleToken(uint id, ulong accessToken) { if (!SecretTokens.ContainsKey(id)) { SecretTokens.Add(id, accessToken); IRC.Instance.SendOps("[TOKENS] Added a new token that the bot got itself: {0} ({1})", id, Steam.GetAppName(id)); Log.WriteInfo("PICSTokens", "New token for appid {0}", id); using (var db = Database.GetConnection()) { db.Execute("INSERT INTO `PICSTokens` (`AppID`, `Token`) VALUES(@AppID, @Token)", new { AppID = id, Token = accessToken } ); } } else if (SecretTokens[id] != accessToken) { IRC.Instance.SendOps("[TOKENS] Yo xPaw, bot got a token that mismatches the one in database: {0} != {1}", SecretTokens[id], accessToken); } }
/* * Here be dragons. */ public static void DownloadFilesFromDepot(DepotProcessor.ManifestJob job, DepotManifest depotManifest) { var randomGenerator = new Random(); var files = depotManifest.Files.Where(x => IsFileNameMatching(job.DepotID, x.FileName)).ToList(); var filesUpdated = false; var filesAnyFailed = false; var hashesFile = Path.Combine(Application.Path, "files", ".support", "hashes", string.Format("{0}.json", job.DepotID)); var hashes = new Dictionary <string, byte[]>(); if (File.Exists(hashesFile)) { hashes = JsonConvert.DeserializeObject <Dictionary <string, byte[]> >(File.ReadAllText(hashesFile)); } Log.WriteDebug("FileDownloader", "Will download {0} files from depot {1}", files.Count, job.DepotID); Parallel.ForEach(files, new ParallelOptions { MaxDegreeOfParallelism = 2 }, (file, state2) => { string directory = Path.Combine(Application.Path, "files", job.DepotID.ToString(), Path.GetDirectoryName(file.FileName)); string finalPath = Path.Combine(directory, Path.GetFileName(file.FileName)); string downloadPath = Path.Combine(Path.GetTempPath(), Path.ChangeExtension(Path.GetRandomFileName(), ".steamdb_tmp")); if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } else if (file.TotalSize == 0) { if (File.Exists(finalPath)) { var f = new FileInfo(finalPath); if (f.Length == 0) { #if DEBUG Log.WriteDebug("FileDownloader", "{0} is already empty", file.FileName); #endif return; } } else { File.Create(finalPath); Log.WriteInfo("FileDownloader", "{0} created an empty file", file.FileName); return; } } else if (hashes.ContainsKey(file.FileName) && file.FileHash.SequenceEqual(hashes[file.FileName])) { #if DEBUG Log.WriteDebug("FileDownloader", "{0} already matches the file we have", file.FileName); #endif return; } var chunks = file.Chunks.OrderBy(x => x.Offset).ToList(); Log.WriteInfo("FileDownloader", "Downloading {0} ({1} bytes, {2} chunks)", file.FileName, file.TotalSize, chunks.Count); uint count = 0; byte[] checksum; string lastError = "or checksum failed"; string oldChunksFile; using (var sha = new SHA1Managed()) { oldChunksFile = Path.Combine(Application.Path, "files", ".support", "chunks", string.Format("{0}-{1}.json", job.DepotID, BitConverter.ToString(sha.ComputeHash(Encoding.UTF8.GetBytes(file.FileName)))) ); } using (var fs = File.Open(downloadPath, FileMode.OpenOrCreate, FileAccess.ReadWrite)) { fs.SetLength((long)file.TotalSize); var lockObject = new object(); var neededChunks = new List <DepotManifest.ChunkData>(); if (File.Exists(oldChunksFile) && File.Exists(finalPath)) { var oldChunks = JsonConvert.DeserializeObject <List <DepotManifest.ChunkData> >( File.ReadAllText(oldChunksFile), new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.All } ); using (var fsOld = File.Open(finalPath, FileMode.Open, FileAccess.Read)) { foreach (var chunk in chunks) { var oldChunk = oldChunks.FirstOrDefault(c => c.ChunkID.SequenceEqual(chunk.ChunkID)); if (oldChunk != null) { var oldData = new byte[oldChunk.UncompressedLength]; fsOld.Seek((long)oldChunk.Offset, SeekOrigin.Begin); fsOld.Read(oldData, 0, oldData.Length); var existingChecksum = Utils.AdlerHash(oldData); if (existingChecksum.SequenceEqual(chunk.Checksum)) { fs.Seek((long)chunk.Offset, SeekOrigin.Begin); fs.Write(oldData, 0, oldData.Length); #if DEBUG Log.WriteDebug("FileDownloader", "{0} Found chunk ({1}), not downloading ({2}/{3})", file.FileName, chunk.Offset, ++count, chunks.Count); #endif } else { neededChunks.Add(chunk); #if DEBUG Log.WriteDebug("FileDownloader", "{0} Found chunk ({1}), but checksum differs", file.FileName, chunk.Offset); #endif } } else { neededChunks.Add(chunk); } } } } else { neededChunks = chunks; } Parallel.ForEach(neededChunks, new ParallelOptions { MaxDegreeOfParallelism = 3 }, (chunk, state) => { var downloaded = false; for (var i = 0; i <= 5; i++) { try { var chunkData = CDNClient.DownloadDepotChunk(job.DepotID, chunk, job.Server, job.CDNToken, job.DepotKey); lock (lockObject) { fs.Seek((long)chunk.Offset, SeekOrigin.Begin); fs.Write(chunkData.Data, 0, chunkData.Data.Length); Log.WriteDebug("FileDownloader", "Downloaded {0} ({1}/{2})", file.FileName, ++count, chunks.Count); } downloaded = true; break; } catch (Exception e) { lastError = e.Message; } // See https://developers.google.com/drive/web/handle-errors Task.Delay((1 << i) * 1000 + randomGenerator.Next(1001)).Wait(); } if (!downloaded) { state.Stop(); } }); fs.Seek(0, SeekOrigin.Begin); using (var sha = new SHA1Managed()) { checksum = sha.ComputeHash(fs); } } if (file.FileHash.SequenceEqual(checksum)) { Log.WriteInfo("FileDownloader", "Downloaded {0} from {1}", file.FileName, Steam.GetAppName(job.ParentAppID)); hashes[file.FileName] = checksum; if (File.Exists(finalPath)) { File.Delete(finalPath); } File.Move(downloadPath, finalPath); if (chunks.Count > 1) { File.WriteAllText(oldChunksFile, JsonConvert.SerializeObject( chunks, Formatting.None, new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.All } ) ); } else if (File.Exists(oldChunksFile)) { File.Delete(oldChunksFile); } filesUpdated = true; } else { filesAnyFailed = true; IRC.Instance.SendOps("{0}[{1}]{2} Failed to download {3}: Only {4} out of {5} chunks downloaded ({6})", Colors.OLIVE, Steam.GetAppName(job.ParentAppID), Colors.NORMAL, file.FileName, count, chunks.Count, lastError); Log.WriteError("FileDownloader", "Failed to download {0}: Only {1} out of {2} chunks downloaded from {3} ({4})", file.FileName, count, chunks.Count, job.Server, lastError); File.Delete(downloadPath); } }); if (filesUpdated) { if (filesAnyFailed) { IRC.Instance.SendOps("{0}[{1}]{2} Failed to download some files, not running update script to prevent broken diffs.", Colors.OLIVE, Steam.GetAppName(job.ParentAppID), Colors.NORMAL); return; } File.WriteAllText(hashesFile, JsonConvert.SerializeObject(hashes)); var updateScript = Path.Combine(Application.Path, "files", "update.sh"); if (File.Exists(updateScript)) { lock (updateLock) { // YOLO Process.Start(updateScript, job.DepotID.ToString()); } } } }
private static async Task <DateTime> ProcessFeed(Uri feedUrl, DateTime lastPostDate) { var feed = await LoadRSS(feedUrl); if (feed == null) { return(DateTime.MinValue); } if (feed.Items.Count == 0) { Log.WriteError(nameof(RSS), $"Did not find any items in {feedUrl}"); return(DateTime.MinValue); } await using var db = await Database.GetConnectionAsync(); var items = (await db.QueryAsync <GenericFeedItem>("SELECT `Link` FROM `RSS` WHERE `Link` IN @Ids", new { Ids = feed.Items.Select(x => x.Link) })).ToDictionary(x => x.Link, _ => (byte)1); var newItems = feed.Items.Where(item => item.PubDate > lastPostDate && !items.ContainsKey(item.Link)); var maxDate = DateTime.MinValue; foreach (var item in newItems) { if (maxDate < item.PubDate) { maxDate = item.PubDate; } Log.WriteInfo(nameof(RSS), $"[{feed.Title}] {item.Title}: {item.Link} ({item.PubDate})"); IRC.Instance.SendAnnounce($"{Colors.BLUE}{feed.Title}{Colors.NORMAL}: {item.Title} -{Colors.DARKBLUE} {item.Link}"); await db.ExecuteAsync("INSERT INTO `RSS` (`Link`, `Title`, `Date`) VALUES(@Link, @Title, @PubDate)", new { item.Link, item.Title, item.PubDate, }); _ = TaskManager.Run(async() => await Utils.SendWebhook(new { Type = "RSS", item.Title, Url = item.Link, })); if (!Settings.IsMillhaven) { continue; } uint appID = 0; if (feed.Title == "Steam RSS News Feed") { if (item.Title.StartsWith("Dota 2 Update", StringComparison.Ordinal)) { appID = 570; } else if (item.Title == "Team Fortress 2 Update Released") { appID = 440; // tf2 changelog cleanup item.Content = item.Content.Replace("<br/>", "\n"); item.Content = item.Content.Replace("<ul style=\"padding-bottom: 0px; margin-bottom: 0px;\">", "\n"); item.Content = item.Content.Replace("<ul style=\"padding-bottom: 0px; margin-bottom: 0px;\" >", "\n"); item.Content = item.Content.Replace("</ul>", "\n"); item.Content = item.Content.Replace("<li>", "* "); } else if (item.Title == "Left 4 Dead 2 - Update") { appID = 550; } else if (item.Title == "Left 4 Dead - Update") { appID = 500; } else if (item.Title == "Portal 2 - Update") { appID = 620; } } else if (feed.Title.Contains("Counter-Strike: Global Offensive") && item.Title.StartsWith("Release Notes", StringComparison.Ordinal)) { appID = 730; // csgo changelog cleanup item.Content = item.Content.Replace("</p>", "\n"); item.Content = new Regex("<p>\\[\\s*(.+)\\s*\\]", RegexOptions.Multiline | RegexOptions.CultureInvariant).Replace(item.Content, "## $1"); item.Content = item.Content.Replace("<p>", ""); } if (appID > 0) { var build = (await db.QueryAsync <Build>( "SELECT `Builds`.`BuildID`, `Builds`.`ChangeID`, `Builds`.`AppID`, `Changelists`.`Date`, LENGTH(`Official`) as `Official` FROM `Builds` " + "LEFT JOIN `Patchnotes` ON `Patchnotes`.`BuildID` = `Builds`.`BuildID` " + "JOIN `Apps` ON `Apps`.`AppID` = `Builds`.`AppID` " + "JOIN `Changelists` ON `Builds`.`ChangeID` = `Changelists`.`ChangeID` " + "WHERE `Builds`.`AppID` = @AppID ORDER BY `Builds`.`BuildID` DESC LIMIT 1", new { appID } )).SingleOrDefault(); if (build == null) { continue; } if (DateTime.UtcNow > build.Date.AddMinutes(60)) { Log.WriteDebug(nameof(RSS), $"Got {appID} update patch notes, but there is no build within last 10 minutes. {item.Link}"); IRC.Instance.SendOps($"{Colors.GREEN}[Patch notes]{Colors.NORMAL} Got {appID} update patch notes, but there is no build within last 10 minutes. {item.Link}"); continue; } if (build.Official > 0) { Log.WriteDebug(nameof(RSS), $"Got {appID} update patch notes, but official patch notes is already filled. {item.Link}"); IRC.Instance.SendOps($"{Colors.GREEN}[Patch notes]{Colors.NORMAL} Got {appID} update patch notes, but official patch notes is already filled. {item.Link}"); continue; } // breaks item.Content = new Regex(@"<br( \/)?>\r?\n?", RegexOptions.Multiline | RegexOptions.CultureInvariant).Replace(item.Content, "\n"); // dashes (CS:GO mainly) item.Content = new Regex("^(?:-|&#(?:8208|8209|8210|8211|8212|8213);|–|—) ?", RegexOptions.Multiline | RegexOptions.CultureInvariant).Replace(item.Content, "* "); item.Content = WebUtility.HtmlDecode(item.Content); Log.WriteDebug(nameof(RSS), $"Inserting {build.AppID} patchnotes for build {build.BuildID}:\n{item.Content}"); var accountId = Steam.Instance.Client?.SteamID?.AccountID ?? 0; await db.ExecuteAsync( "INSERT INTO `Patchnotes` (`BuildID`, `AppID`, `ChangeID`, `Date`, `Official`, `OfficialURL`) " + "VALUES (@BuildID, @AppID, @ChangeID, @Date, @Content, @Link) ON DUPLICATE KEY UPDATE `Official` = VALUES(`Official`), `OfficialURL` = VALUES(`OfficialURL`), `LastEditor` = @AccountID", new { build.BuildID, build.AppID, build.ChangeID, Date = build.Date.AddSeconds(1).ToString("yyyy-MM-dd HH:mm:ss"), item.Content, item.Link, accountId } ); IRC.Instance.SendAnnounce($"\u2699 Official patch notes:{Colors.BLUE} {Steam.GetAppName(build.AppID)}{Colors.NORMAL} -{Colors.DARKBLUE} {SteamDB.GetPatchnotesUrl(build.BuildID)}"); } } return(maxDate); }
private static void OnPICSProductInfo(SteamApps.PICSProductInfoCallback callback) { JobAction job; if (!JobManager.TryRemoveJob(callback.JobID, out job) || !job.IsCommand) { return; } var request = job.CommandRequest; if (request.Type == JobManager.IRCRequestType.TYPE_SUB) { if (!callback.Packages.ContainsKey(request.Target)) { CommandHandler.ReplyToCommand(request.Command, "Unknown SubID: {0}{1}{2}", Colors.BLUE, request.Target, LicenseList.OwnedSubs.ContainsKey(request.Target) ? SteamDB.StringCheckmark : string.Empty); return; } var info = callback.Packages[request.Target]; var kv = info.KeyValues.Children.FirstOrDefault(); string name; if (kv["name"].Value != null) { name = Utils.RemoveControlCharacters(kv["name"].AsString()); } else { name = Steam.GetPackageName(info.ID); } try { kv.SaveToFile(Path.Combine(Application.Path, "sub", string.Format("{0}.vdf", info.ID)), false); } catch (Exception e) { CommandHandler.ReplyToCommand(request.Command, "Unable to save file for {0}: {1}", name, e.Message); return; } CommandHandler.ReplyToCommand(request.Command, "{0}{1}{2} -{3} {4}{5} - Dump:{6} {7}{8}{9}{10}", Colors.BLUE, name, Colors.NORMAL, Colors.DARKBLUE, SteamDB.GetPackageURL(info.ID), Colors.NORMAL, Colors.DARKBLUE, SteamDB.GetRawPackageURL(info.ID), Colors.NORMAL, info.MissingToken ? SteamDB.StringNeedToken : string.Empty, LicenseList.OwnedSubs.ContainsKey(info.ID) ? SteamDB.StringCheckmark : string.Empty ); } else if (request.Type == JobManager.IRCRequestType.TYPE_APP) { if (!callback.Apps.ContainsKey(request.Target)) { CommandHandler.ReplyToCommand(request.Command, "Unknown AppID: {0}{1}{2}", Colors.BLUE, request.Target, LicenseList.OwnedApps.ContainsKey(request.Target) ? SteamDB.StringCheckmark : string.Empty); return; } var info = callback.Apps[request.Target]; string name; if (info.KeyValues["common"]["name"].Value != null) { name = Utils.RemoveControlCharacters(info.KeyValues["common"]["name"].AsString()); } else { name = Steam.GetAppName(info.ID); } try { info.KeyValues.SaveToFile(Path.Combine(Application.Path, "app", string.Format("{0}.vdf", info.ID)), false); } catch (Exception e) { CommandHandler.ReplyToCommand(request.Command, "Unable to save file for {0}: {1}", name, e.Message); return; } CommandHandler.ReplyToCommand(request.Command, "{0}{1}{2} -{3} {4}{5} - Dump:{6} {7}{8}{9}{10}", Colors.BLUE, name, Colors.NORMAL, Colors.DARKBLUE, SteamDB.GetAppURL(info.ID), Colors.NORMAL, Colors.DARKBLUE, SteamDB.GetRawAppURL(info.ID), Colors.NORMAL, info.MissingToken ? SteamDB.StringNeedToken : string.Empty, LicenseList.OwnedApps.ContainsKey(info.ID) ? SteamDB.StringCheckmark : string.Empty ); } else { CommandHandler.ReplyToCommand(request.Command, "I have no idea what happened here!"); } }
public override async Task OnCommand(CommandArguments command) { if (command.CommandType != ECommandType.IRC || !IRC.IsRecipientChannel(command.Recipient)) { command.Reply("This command is only available in channels."); return; } var channel = command.Recipient; var s = command.Message.Split(' '); var count = s.Length; if (count > 0) { uint id; switch (s[0]) { case "reload": Application.ReloadImportant(command); PICSTokens.Reload(command); FileDownloader.ReloadFileList(); return; case "add": if (count < 3) { break; } if (!uint.TryParse(s[2], out id)) { break; } switch (s[1]) { case "app": var exists = Application.ImportantApps.TryGetValue(id, out var channels); if (exists && channels.Contains(channel)) { command.Reply("App {0}{1}{2} ({3}) is already important in {4}{5}{6}.", Colors.BLUE, id, Colors.NORMAL, Steam.GetAppName(id), Colors.BLUE, channel, Colors.NORMAL); } else { if (exists) { Application.ImportantApps[id].Add(channel); } else { Application.ImportantApps.Add(id, new List <string> { channel }); } await using (var db = await Database.GetConnectionAsync()) { await db.ExecuteAsync("INSERT INTO `ImportantApps` (`AppID`, `Channel`) VALUES (@AppID, @Channel)", new { AppID = id, Channel = channel }); } command.Reply("Marked app {0}{1}{2} ({3}) as important in {4}{5}{6}.", Colors.BLUE, id, Colors.NORMAL, Steam.GetAppName(id), Colors.BLUE, channel, Colors.NORMAL); } return; case "sub": if (Application.ImportantSubs.ContainsKey(id)) { command.Reply("Package {0}{1}{2} ({3}) is already important.", Colors.BLUE, id, Colors.NORMAL, Steam.GetPackageName(id)); } else { Application.ImportantSubs.Add(id, 1); await using (var db = await Database.GetConnectionAsync()) { await db.ExecuteAsync("INSERT INTO `ImportantSubs` (`SubID`) VALUES (@SubID)", new { SubID = id }); } command.Reply("Marked package {0}{1}{2} ({3}) as important.", Colors.BLUE, id, Colors.NORMAL, Steam.GetPackageName(id)); } return; } break; case "remove": if (count < 3) { break; } if (!uint.TryParse(s[2], out id)) { break; } switch (s[1]) { case "app": if (!Application.ImportantApps.TryGetValue(id, out var channels) || !channels.Contains(channel)) { command.Reply("App {0}{1}{2} ({3}) is not important in {4}{5}{6}.", Colors.BLUE, id, Colors.NORMAL, Steam.GetAppName(id), Colors.BLUE, channel, Colors.NORMAL); } else { if (channels.Count > 1) { Application.ImportantApps[id].Remove(channel); } else { Application.ImportantApps.Remove(id); } await using (var db = await Database.GetConnectionAsync()) { db.Execute("DELETE FROM `ImportantApps` WHERE `AppID` = @AppID AND `Channel` = @Channel", new { AppID = id, Channel = channel }); } command.Reply("Removed app {0}{1}{2} ({3}) from the important list in {4}{5}{6}.", Colors.BLUE, id, Colors.NORMAL, Steam.GetAppName(id), Colors.BLUE, channel, Colors.NORMAL); } return; case "sub": if (!Application.ImportantSubs.ContainsKey(id)) { command.Reply("Package {0}{1}{2} ({3}) is not important.", Colors.BLUE, id, Colors.NORMAL, Steam.GetPackageName(id)); } else { Application.ImportantSubs.Remove(id); await using (var db = await Database.GetConnectionAsync()) { await db.ExecuteAsync("DELETE FROM `ImportantSubs` WHERE `SubID` = @SubID", new { SubID = id }); } command.Reply("Removed package {0}{1}{2} ({3}) from the important list.", Colors.BLUE, id, Colors.NORMAL, Steam.GetPackageName(id)); } return; } break; } } command.Reply("Usage:{0} important reload {1}or{2} important <add/remove> <app/sub> <id>", Colors.OLIVE, Colors.NORMAL, Colors.OLIVE); }
public override async Task OnCommand(CommandArguments command) { if (command.Message.Length == 0) { command.Reply("Usage:{0} players <appid or partial game name>", Colors.OLIVE); return; } string name; if (!uint.TryParse(command.Message, out var appID)) { appID = await AppCommand.TrySearchAppId(command); if (appID == 0) { command.Reply("Nothing was found matching your request."); return; } } var callback = await Steam.Instance.UserStats.GetNumberOfCurrentPlayers(appID); if (appID == 0) { appID = 753; } name = Steam.GetAppName(appID, out var appType); if (callback.Result != EResult.OK) { command.Reply($"Unable to request player count for {Colors.BLUE}{name}{Colors.NORMAL}: {Colors.RED}{callback.Result}{Colors.NORMAL} -{Colors.DARKBLUE} {SteamDB.GetAppURL(appID, "graphs")}"); return; } var type = "playing"; switch (appType) { case "Tool": case "Config": case "Application": type = "using"; break; case "Legacy Media": case "Series": case "Video": type = "watching"; break; case "Demo": type = "demoing"; break; case "Guide": type = "reading"; break; case "Hardware": type = "bricking"; break; } command.Reply( "{0}{1:N0}{2} {3} {4}{5}{6}{7} -{8} {9}", Colors.OLIVE, callback.NumPlayers, Colors.NORMAL, type, Colors.BLUE, name, Colors.NORMAL, callback.Result != EResult.OK ? $" {Colors.RED}({callback.Result}){Colors.NORMAL}" : "", Colors.DARKBLUE, SteamDB.GetAppURL(appID, "graphs") ); }