From f7c044ef79ded0c22a9c3cbfa4a8528332ce3625 Mon Sep 17 00:00:00 2001 From: C-xC-c Date: Tue, 10 Dec 2019 22:07:17 +0000 Subject: [PATCH] Initial commit --- .gitattributes | 63 ++++ .gitignore | 342 ++++++++++++++++++ BantFlags.sln | 25 ++ BantFlags/.config/dotnet-tools.json | 12 + BantFlags/BantFlags.csproj | 27 ++ BantFlags/Controllers/FlagsController.cs | 101 ++++++ BantFlags/Data/DatabaseService.cs | 95 +++++ BantFlags/Data/FlagModel.cs | 16 + BantFlags/Data/IEnumerableExtensions.cs | 16 + BantFlags/Data/MySqlConnectionPool.cs | 57 +++ BantFlags/Data/MySqlExtensions.cs | 44 +++ BantFlags/Data/Pool.cs | 25 ++ BantFlags/Data/Query.cs | 61 ++++ BantFlags/Pages/Index.cshtml | 9 + BantFlags/Pages/Index.cshtml.cs | 17 + BantFlags/Pages/Shared/_Layout.cshtml | 12 + BantFlags/Program.cs | 32 ++ BantFlags/Properties/launchSettings.json | 30 ++ BantFlags/Startup.cs | 67 ++++ BantFlags/appsettings.example.json | 17 + BantFlags/wwwroot/bantflags.meta.js | 21 ++ BantFlags/wwwroot/bantflags.user.js | 421 +++++++++++++++++++++++ database.sql | 82 +++++ 23 files changed, 1592 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 BantFlags.sln create mode 100644 BantFlags/.config/dotnet-tools.json create mode 100644 BantFlags/BantFlags.csproj create mode 100644 BantFlags/Controllers/FlagsController.cs create mode 100644 BantFlags/Data/DatabaseService.cs create mode 100644 BantFlags/Data/FlagModel.cs create mode 100644 BantFlags/Data/IEnumerableExtensions.cs create mode 100644 BantFlags/Data/MySqlConnectionPool.cs create mode 100644 BantFlags/Data/MySqlExtensions.cs create mode 100644 BantFlags/Data/Pool.cs create mode 100644 BantFlags/Data/Query.cs create mode 100644 BantFlags/Pages/Index.cshtml create mode 100644 BantFlags/Pages/Index.cshtml.cs create mode 100644 BantFlags/Pages/Shared/_Layout.cshtml create mode 100644 BantFlags/Program.cs create mode 100644 BantFlags/Properties/launchSettings.json create mode 100644 BantFlags/Startup.cs create mode 100644 BantFlags/appsettings.example.json create mode 100644 BantFlags/wwwroot/bantflags.meta.js create mode 100644 BantFlags/wwwroot/bantflags.user.js create mode 100644 database.sql diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30e21f5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,342 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +appsettings.json + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +/BantFlags/wwwroot/flags + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- Backup*.rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb diff --git a/BantFlags.sln b/BantFlags.sln new file mode 100644 index 0000000..259451e --- /dev/null +++ b/BantFlags.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29411.108 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BantFlags", "BantFlags\BantFlags.csproj", "{DDDE290B-44A9-485A-B0F4-E7D696EAFF8D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DDDE290B-44A9-485A-B0F4-E7D696EAFF8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DDDE290B-44A9-485A-B0F4-E7D696EAFF8D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DDDE290B-44A9-485A-B0F4-E7D696EAFF8D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DDDE290B-44A9-485A-B0F4-E7D696EAFF8D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {252385AC-0BBA-43F2-858A-BA3877F33106} + EndGlobalSection +EndGlobal diff --git a/BantFlags/.config/dotnet-tools.json b/BantFlags/.config/dotnet-tools.json new file mode 100644 index 0000000..6b2cf2d --- /dev/null +++ b/BantFlags/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "3.1.0", + "commands": [ + "dotnet-ef" + ] + } + } +} \ No newline at end of file diff --git a/BantFlags/BantFlags.csproj b/BantFlags/BantFlags.csproj new file mode 100644 index 0000000..6f2037d --- /dev/null +++ b/BantFlags/BantFlags.csproj @@ -0,0 +1,27 @@ + + + + netcoreapp3.1 + Manx + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + \ No newline at end of file diff --git a/BantFlags/Controllers/FlagsController.cs b/BantFlags/Controllers/FlagsController.cs new file mode 100644 index 0000000..214a402 --- /dev/null +++ b/BantFlags/Controllers/FlagsController.cs @@ -0,0 +1,101 @@ +using BantFlags.Data; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Threading.Tasks; + +namespace BantFlags.Controllers +{ + [ApiController] + [Route("api")] + public class FlagsController : Controller + { + private DatabaseService Database { get; } + + private ILogger Logger { get; } + + private string FlagList { get; set; } + + private HashSet DatabaseFlags { get; set; } + + public FlagsController(DatabaseService db, ILogger logger) + { + Database = db; + Logger = logger; + + // During initialisation we get the current list of flags for + // resolving supported flags and preventing duplicate flags from + // being created + List flags = Database.GetFlags().Result; + + FlagList = string.Join("\n", flags); + DatabaseFlags = flags.ToHashSet(); + } + + [HttpPost] + [Route("get")] + [Consumes("application/x-www-form-urlencoded")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task Get([FromForm]string post_nrs, [FromForm]string board, [FromForm]string version) + { + try + { + var posts = await Database.GetPosts(post_nrs); + + return Json(posts); + } + catch (Exception e) + { + return Problem(e.Message, statusCode: StatusCodes.Status400BadRequest); // TODO: We shouldn't send the exception message + } + } + + [HttpPost] + [Route("post")] + [Consumes("application/x-www-form-urlencoded")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task Post([FromForm]string post_nr, [FromForm]string board, [FromForm]string regions) + { + try // We only care if the post if valid. + { + // TODO: Currently we skip over invalid flags. Should we error instead? + var flags = regions.Split("||").Where(x => DatabaseFlags.Contains(x)); + + FlagModel post = new FlagModel + { + PostNumber = int.TryParse(post_nr, out int temp) ? temp : throw new FormatException("Bad post number."), + Board = board == "bant" ? "bant" : throw new FormatException("Board parameter wasn't formatted correctly."), + Flags = flags.Count() > 0 ? flags : throw new FormatException("Your post didn't include any flags, or your flags were invalid.") + }; + + await Database.InsertPost(post); + + return Ok(post); + } + catch (Exception e) + { + return Problem(detail: ErrorMessage(e), statusCode: StatusCodes.Status400BadRequest); + } + } + + [HttpGet] + [Route("flags")] + public IActionResult Flags() => Ok(FlagList); + + private string ErrorMessage(Exception exception) => + exception switch + { + FormatException e => e.Message, + DbException _ => "Internal database error.", + ArgumentNullException _ => "No regions sent", + Exception e => e.Message, // Don't do this. + _ => "how in the hell" + }; // This needs more testing. + } +} \ No newline at end of file diff --git a/BantFlags/Data/DatabaseService.cs b/BantFlags/Data/DatabaseService.cs new file mode 100644 index 0000000..69c03a1 --- /dev/null +++ b/BantFlags/Data/DatabaseService.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; + +namespace BantFlags.Data +{ + public class DatabaseService + { + private MySqlConnectionPool ConnectionPool { get; } + + public DatabaseService(DatabaseServiceConfig dbConfig) + { + ConnectionPool = new MySqlConnectionPool(dbConfig.ConnectionString, dbConfig.PoolSize); + } + + private readonly string SelectQuery = @"SELECT posts.post_nr, flags.flag FROM flags LEFT JOIN (postflags) ON (postflags.flag = flags.id) LEFT JOIN (posts) ON (postflags.post_nr = posts.id) WHERE FIND_IN_SET(posts.post_nr, (@posts))"; + + public async Task>> GetPosts(string input) + { + List> posts = new List>(); + + using (var rentedConnection = await ConnectionPool.RentConnectionAsync()) + { + DataTable table = await rentedConnection.Object.CreateQuery(SelectQuery) + .SetParam("@posts", input) + .ExecuteTableAsync(); + + // TODO: rework this. + // Once the majority are on the new script we can do the below + // and return Dictionary> + // instead of rewriting the flags each time. + var groupedPosts = table.AsEnumerable() + .GroupBy(x => x.GetValue("post_nr")); + + //.ToDictionary( + // x => x.Key, + // x => x.AsEnumerable().Select(x => x.GetValue("flag")); + + groupedPosts.ForEach(x => posts.Add( + new Dictionary + { + {"post_nr", x.Key.ToString() }, + // This is a lot of work, it'll be nice to get rid of it. + {"region", string.Join("||", x.AsEnumerable().Select(y => y.GetValue("flag")))} + } + )); + + return posts; + } + } + + public async Task InsertPost(FlagModel post) + { + using (var rentedConnection = await ConnectionPool.RentConnectionAsync()) + { + await rentedConnection.Object.UseStoredProcedure("insert_post") + .SetParam("@post_nr", post.PostNumber) + .SetParam("@board", post.Board) + .ExecuteNonQueryAsync(); + + using (var query = rentedConnection.Object.UseStoredProcedure("insert_post_flags")) + { + query.SetParam("@post_nr", post.PostNumber); + + post.Flags.ForEach(async f => + await query.SetParam("@flag", f) + .ExecuteNonQueryAsync(reuse: true)); + } + } + + return; + } + + public async Task> GetFlags() + { + using (var rentedConnected = await ConnectionPool.RentConnectionAsync()) + { + DataTable table = await rentedConnected.Object.CreateQuery("SELECT flags.flag FROM flags") + .ExecuteTableAsync(); + + return table.AsEnumerable() + .Select(x => x.GetValue("flag")) + .ToList(); + } + } + } + + public class DatabaseServiceConfig + { + public string ConnectionString { get; set; } + + public int PoolSize { get; set; } + } +} \ No newline at end of file diff --git a/BantFlags/Data/FlagModel.cs b/BantFlags/Data/FlagModel.cs new file mode 100644 index 0000000..5954099 --- /dev/null +++ b/BantFlags/Data/FlagModel.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BantFlags.Data +{ + public class FlagModel + { + public int PostNumber { get; set; } + + public string Board { get; set; } + + public IEnumerable Flags { get; set; } + } +} \ No newline at end of file diff --git a/BantFlags/Data/IEnumerableExtensions.cs b/BantFlags/Data/IEnumerableExtensions.cs new file mode 100644 index 0000000..6e859f2 --- /dev/null +++ b/BantFlags/Data/IEnumerableExtensions.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace BantFlags.Data +{ + public static class IEnumerableExtensions + { + public static void ForEach(this IEnumerable enumeration, Action action) + { + foreach (T item in enumeration) + { + action(item); + } + } + } +} \ No newline at end of file diff --git a/BantFlags/Data/MySqlConnectionPool.cs b/BantFlags/Data/MySqlConnectionPool.cs new file mode 100644 index 0000000..1d36d09 --- /dev/null +++ b/BantFlags/Data/MySqlConnectionPool.cs @@ -0,0 +1,57 @@ +using MySql.Data.MySqlClient; +using Nito.AsyncEx; +using System; +using System.Data; +using System.Threading.Tasks; + +namespace BantFlags.Data +{ + public class MySqlConnectionPool : IDisposable + { + public AsyncCollection Connections { get; } + + protected string ConnectionString { get; } + + protected int PoolSize { get; } + + public MySqlConnectionPool(string connectionString, int poolSize) + { + PoolSize = poolSize; + ConnectionString = connectionString; + + Connections = new AsyncCollection(poolSize); + + for (int i = 0; i < poolSize; i++) + { + var connection = new MySqlConnection(connectionString); + + connection.Open(); + + Connections.Add(connection); + } + } + + public async Task> RentConnectionAsync() + { + return new PoolObject(await Connections.TakeAsync(), obj => + { + if (obj.State != ConnectionState.Open) + { + obj.Open(); + } + + Connections.Add(obj); + }); + } + + public void Dispose() + { + for (int i = 0; i < PoolSize; i++) + { + var connection = Connections.Take(); + + connection.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/BantFlags/Data/MySqlExtensions.cs b/BantFlags/Data/MySqlExtensions.cs new file mode 100644 index 0000000..e9b9fa7 --- /dev/null +++ b/BantFlags/Data/MySqlExtensions.cs @@ -0,0 +1,44 @@ +using MySql.Data.MySqlClient; +using System; +using System.Data; + +namespace BantFlags.Data +{ + public static class MySqlExtensions + { + public static Query CreateQuery(this MySqlConnection connection, string sql) + { + connection.EnsureConnectionIsOpen(); + + return new Query(new MySqlCommand(sql, connection)); + } + + public static Query UseStoredProcedure(this MySqlConnection connection, string sql) + { + connection.EnsureConnectionIsOpen(); + + return new Query(new MySqlCommand(sql, connection) + { + CommandType = CommandType.StoredProcedure + }); + } + + public static T GetValue(this DataRow row, string column) + { + object value = row[column]; + + if (value == null || value == DBNull.Value) + return default; + + return (T)value; + } + + private static void EnsureConnectionIsOpen(this MySqlConnection connection) + { + if (connection.State != ConnectionState.Open) + { + connection.Open(); + } + } + } +} \ No newline at end of file diff --git a/BantFlags/Data/Pool.cs b/BantFlags/Data/Pool.cs new file mode 100644 index 0000000..2cdf397 --- /dev/null +++ b/BantFlags/Data/Pool.cs @@ -0,0 +1,25 @@ +using System; + +namespace BantFlags.Data +{ + public class PoolObject : IDisposable + { + public T Object { get; } + + private Action ReturnAction { get; } + + public PoolObject(T o, Action returnAction) + { + Object = o; + ReturnAction = returnAction; + } + + public void Dispose() + { + ReturnAction(Object); + } + + public static implicit operator T(PoolObject poolObject) + => poolObject.Object; + } +} \ No newline at end of file diff --git a/BantFlags/Data/Query.cs b/BantFlags/Data/Query.cs new file mode 100644 index 0000000..69e6c5c --- /dev/null +++ b/BantFlags/Data/Query.cs @@ -0,0 +1,61 @@ +using MySql.Data.MySqlClient; +using System; +using System.Data; +using System.Threading.Tasks; + +namespace BantFlags.Data +{ + /// + /// Succinct methods for creating and executing database queries + /// + public class Query : IDisposable + { + private MySqlCommand Command { get; } + + public Query(MySqlCommand cmd) + { + Command = cmd; + } + + public async Task ExecuteTableAsync() + { + using (var reader = await Command.ExecuteReaderAsync()) + { + DataTable table = new DataTable(); + table.Load(reader); + + return table; + } + } + + public async Task ExecuteNonQueryAsync(bool reuse = false) + { + await Command.ExecuteNonQueryAsync(); + + if (!reuse) + { + Dispose(); + } + } + + public Query SetParam(string parameter, object value) + { + // When we reuse a query, we write over the parameter. + if (Command.Parameters.Contains(parameter)) + { + Command.Parameters[parameter].Value = value; + } + else + { + Command.Parameters.AddWithValue(parameter, value); + } + + return this; + } + + public void Dispose() + { + Command.Dispose(); + } + } +} \ No newline at end of file diff --git a/BantFlags/Pages/Index.cshtml b/BantFlags/Pages/Index.cshtml new file mode 100644 index 0000000..438db72 --- /dev/null +++ b/BantFlags/Pages/Index.cshtml @@ -0,0 +1,9 @@ +@page +@model BantFlags.Pages.IndexModel +@{ + ViewData["Title"] = "BantFlags"; + Layout = "~/Pages/Shared/_Layout.cshtml"; +} + +

/bant/ Flags

+

Eメール: boku (at) plum (dot) moe

\ No newline at end of file diff --git a/BantFlags/Pages/Index.cshtml.cs b/BantFlags/Pages/Index.cshtml.cs new file mode 100644 index 0000000..984d628 --- /dev/null +++ b/BantFlags/Pages/Index.cshtml.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace BantFlags.Pages +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/BantFlags/Pages/Shared/_Layout.cshtml b/BantFlags/Pages/Shared/_Layout.cshtml new file mode 100644 index 0000000..fcd2a8c --- /dev/null +++ b/BantFlags/Pages/Shared/_Layout.cshtml @@ -0,0 +1,12 @@ + + + + + + @ViewData["Title"] + @RenderSection("Head", required: false) + + + @RenderBody() + + \ No newline at end of file diff --git a/BantFlags/Program.cs b/BantFlags/Program.cs new file mode 100644 index 0000000..89cec05 --- /dev/null +++ b/BantFlags/Program.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.IO; + +namespace BantFlags +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + // TODO: set this up properly for use on Debian. + webBuilder.ConfigureLogging(logBuilder => + logBuilder.AddFile(@"/var/log/bantflags.log", minimumLevel: LogLevel.Information)); + webBuilder.UseStartup(); + }) + .ConfigureAppConfiguration((host, config) => + { + // Explicitly look for appsettings.json in the program's directory + config.AddJsonFile(Path.Join(AppDomain.CurrentDomain.BaseDirectory + "appsettings.json"), optional: false, reloadOnChange: false); + }); + } +} \ No newline at end of file diff --git a/BantFlags/Properties/launchSettings.json b/BantFlags/Properties/launchSettings.json new file mode 100644 index 0000000..d75d4f0 --- /dev/null +++ b/BantFlags/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:11673", + "sslPort": 44366 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "weatherforecast", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "BantFlags": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "weatherforecast", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/BantFlags/Startup.cs b/BantFlags/Startup.cs new file mode 100644 index 0000000..eb2f61c --- /dev/null +++ b/BantFlags/Startup.cs @@ -0,0 +1,67 @@ +using BantFlags.Data; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; + +namespace BantFlags +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers() + .AddNewtonsoftJson(); + + services.AddRazorPages(); + + services.AddSingleton(new DatabaseService(Configuration.GetSection("dbconfig").Get())); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + string webroot = Configuration.GetValue("webroot"); + if (webroot != null) + { + env.WebRootPath = webroot; + env.WebRootFileProvider = new PhysicalFileProvider(env.WebRootPath); + } + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseForwardedHeaders(new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto + }); + + app.UseStaticFiles(new StaticFileOptions + { + ServeUnknownFileTypes = true, + DefaultContentType = "text/plain" + }); + + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + endpoints.MapRazorPages(); + }); + } + } +} \ No newline at end of file diff --git a/BantFlags/appsettings.example.json b/BantFlags/appsettings.example.json new file mode 100644 index 0000000..2992548 --- /dev/null +++ b/BantFlags/appsettings.example.json @@ -0,0 +1,17 @@ +{ + "AllowedHosts": "*", + "dbconfig": { + "connectionstring": "Server=localhost;Port=3306;User ID=user;Password=default;Database=bantflags", + "poolsize": 2 + }, + + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + + "webroot": "" +} \ No newline at end of file diff --git a/BantFlags/wwwroot/bantflags.meta.js b/BantFlags/wwwroot/bantflags.meta.js new file mode 100644 index 0000000..8d66d2b --- /dev/null +++ b/BantFlags/wwwroot/bantflags.meta.js @@ -0,0 +1,21 @@ +// ==UserScript== +// @name BantFlags +// @namespace BintFlegs +// @description More flags for r/banter +// @include http*://boards.4chan.org/bant/* +// @include http*://archive.nyafuu.org/bant/* +// @include http*://archived.moe/bant/* +// @include http*://thebarchive.com/bant/* +// @exclude http*://boards.4chan.org/bant/catalog +// @exclude http*://archive.nyafuu.org/bant/statistics/ +// @exclude http*://archived.moe/bant/statistics/ +// @exclude http*://thebarchive.com/bant/statistics/ +// @version 0.8.0 +// @grant GM_xmlhttpRequest +// @grant GM_getValue +// @grant GM_setValue +// @run-at document-end +// @icon https://nineball.party/files/flags/actual_flags/0077.png +// @updateURL https://flags.plum.moe/bantflags.meta.js +// @downloadURL https://flags.plum.moe/bantflags.user.js +// ==/UserScript== \ No newline at end of file diff --git a/BantFlags/wwwroot/bantflags.user.js b/BantFlags/wwwroot/bantflags.user.js new file mode 100644 index 0000000..9f16209 --- /dev/null +++ b/BantFlags/wwwroot/bantflags.user.js @@ -0,0 +1,421 @@ +// ==UserScript== +// @name BantFlags +// @namespace BintFlegs +// @description More flags for r/banter +// @include http*://boards.4chan.org/bant/* +// @include http*://archive.nyafuu.org/bant/* +// @include http*://archived.moe/bant/* +// @include http*://thebarchive.com/bant/* +// @exclude http*://boards.4chan.org/bant/catalog +// @exclude http*://archive.nyafuu.org/bant/statistics/ +// @exclude http*://archived.moe/bant/statistics/ +// @exclude http*://thebarchive.com/bant/statistics/ +// @version 0.8.0 +// @grant GM_xmlhttpRequest +// @grant GM_getValue +// @grant GM_setValue +// @run-at document-end +// @icon https://nineball.party/files/flags/actual_flags/0077.png +// @updateURL https://flags.plum.moe/bantflags.meta.js +// @downloadURL https://flags.plum.moe/bantflags.user.js +// ==/UserScript== + +// This script specifically targets ECMAScript 2015 (const, let, arrow functions). Update your hecking browser. + +// Change this if you want verbose debug information in the console. +const debugMode = true; + +// +// DO NOT EDIT ANYTHING IN THIS SCRIPT DIRECTLY - YOUR FLAGS SHOULD BE CONFIGURED USING THE CONFIGURATION BOXES +// +const postRemoveCounter = 60; +const requestRetryInterval = 5000; // TODO: maybe a max retries counter? +const regionVariable = 'regionVariableAPI2'; // TODO: This is where GM stores flags permanantly. We could use a better name. +const regionDivider = "||"; //TODO: We can probably remove this and seperate by , +const is_archive = window.location.host !== "boards.4chan.org"; +const boardID = "bant"; //TODO: Hardcode /bant/ or accept other boards. +const version = 1; // Breaking changes. +const back_end = 'https://flags.plum.moe/'; +const api_flags = 'api/flags'; +const flag_dir = 'flags/'; +const api_get = 'api/get'; +const api_post = 'api/post'; + +var regions = []; // The flags we have selected. +var postNrs = []; // all post numbers in the thread. + +// +// DO NOT EDIT ANYTHING IN THIS SCRIPT DIRECTLY - YOUR FLAGS SHOULD BE CONFIGURED USING THE CONFIGURATION BOXES +// + +let elementsInClass = x => document.getElementsByClassName(x); +let sliceCall = x => Array.prototype.slice.call(x); +let firstChildInClass = (parent, className) => parent.getElementsByClassName(className)[0]; +let createAndAssign = (element, source) => Object.assign(document.createElement(element), source); + +function addGlobalStyle(css) { + let head = document.getElementsByTagName('head')[0]; + if (!head) { + console.error('[BantFlags] No head tag??'); + return; + } + + head.appendChild(createAndAssign('style', { + type: 'text/css', + innerHTML: css + })); +} + +function debug(text) { + if (debugMode) { + console.log("[BantFlags] " + text); + } +} + +/** Wrapper around GM_xmlhttpRequest + * @param {string} method - The HTTP method (GET, POST) + * @param {string} url - The URL of the request + * @param {string} data - Data sent inn the form body + * @param {Function} func - The function run after onload. Response data is sent directly to it. */ +function MakeRequest(method, url, data, func) { + GM_xmlhttpRequest({ + method: method, + url: url, + data: data, + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + onload: func + }); +} + +function retry(func, resp) { + console.log("[BantFlags] Could not fetch flags, status: " + resp.status); + console.log(resp.statusText); // TODO: surely ASP.NET can return something more useful? + setTimeout(func, requestRetryInterval); +} + +/** nSetup, preferences */ + +// TODO: this shouldn't be a class. +var nsetup = { // not anymore a clone of the original setup + namespace: 'BintFlegs', // TODO: should be const. + flagsLoaded: false, + form: "" + + "" + + "", + fillHtml: function () { // TODO: this function should have a better name. Only called by nsetup.init, can be inlined? + + // resolve flags + MakeRequest( + "GET", + back_end + api_flags, + "", // Because we're GETting. + function (resp) { + debug('Loading flags'); + if (resp.status !== 200) { + retry(nsetup.fillHtml, resp); + return; + } + + let flagSelect = document.getElementById("flagSelect"); + let flagLoad = document.getElementById('flagLoad'); + let x = resp.responseText.split('\n'); + + for (var i = 0; i < x.length; i++) { + let flag = x[i]; + flagSelect.appendChild(createAndAssign('option', { + value: flag, + innerHTML: "" + " " + flag + })); + } + + flagLoad.style.display = 'none'; + flagSelect.style.display = 'inline-block'; + nsetup.flagsLoaded = true; + flagLoad.removeEventListener('click', nsetup.fillHtml); + }); + }, + save: function (k, v) { + GM_setValue(nsetup.namespace + k, v); + regions = nsetup.load(regionVariable); + }, + load: k => GM_getValue(nsetup.namespace + k), // We can get rid of this and just pass regionVariable to GM_getvalue in the two places we need it. + setFlag: function (flag) { // place a flag from the selector to the flags array variable and create an element in the flags_container div + let UID = Math.random().toString(36).substring(7); + let flagName = flag ? flag : document.getElementById("flagSelect").value; + let flagContainer = document.getElementById("bantflags_container"); + + flagContainer.appendChild(createAndAssign('img', { + title: flagName, + src: back_end + flag_dir + flagName + ".png", + id: UID, + className: 'bantflags_flag' + })); + + let flagsCount = flagContainer.children.length; + if (flagsCount > 8) {// TODO: set to constant and enforce server side. + nsetup.gray("on"); + } // Why does 8 work? What happened to the async issue a moment ago? + + document.getElementById(UID).addEventListener("click", function () { + let flagToRemove = document.getElementById(UID); + + flagToRemove.parentNode.removeChild(flagToRemove); + nsetup.gray("off"); + nsetup.save(regionVariable, nsetup.parse()); + }); + + if (!flag) { + nsetup.save(regionVariable, nsetup.parse()); + } + }, + + init: function () { + + // here we insert the form for placing flags. How? + let flagsForm = createAndAssign("div", { + className: 'flagsForm', + innerHTML: nsetup.form + }); + + addGlobalStyle('.flagsForm{float: right; clear: right; margin: 20px 10px;} #flagSelect{display:none;}'); + addGlobalStyle(".bantflags_flag { padding: 1px;} [title^='Romania'] { position: relative; animation: shakeAnim 0.1s linear infinite;} @keyframes shakeAnim { 0% {left: 1px;} 25% {top: 2px;} 50% {left: 1px;} 75% {left: 0px;} 100% {left: 2px;}}"); + + firstChildInClass(document, 'bottomCtrl').parentNode.appendChild(flagsForm); + + for (var i in regions) { + nsetup.setFlag(regions[i]); + } + + document.getElementById("append_flag_button").addEventListener("click", (e) => { + if (nsetup.flagsLoaded) { + nsetup.setFlag(); + } + else { + alert('Load flags before adding them.'); + } + }); + document.getElementById('flagLoad').addEventListener('click', nsetup.fillHtml); + }, + parse: function () { + let flagsArray = []; + let flagElements = elementsInClass("bantflags_flag"); + + for (var i = 0; i < flagElements.length; i++) { + flagsArray[i] = flagElements[i].title; + } + + return flagsArray; + }, + + // TODO: We should pass a bool to this? + gray: state => document.getElementById("append_flag_button").disabled = state === "on" ? true : false +}; + +/** Prompt to set region if regionVariable is empty */ +regions = nsetup.load(regionVariable); // TODO: move this to other init stuff +if (!regions) { + regions = []; + setTimeout(function () { + window.confirm("Bant Flags: No Flags detected"); + }, 2000); +} + +/** parse the posts already on the page before thread updater kicks in */ +function parse4chanPosts() { + let posts = sliceCall(elementsInClass('postContainer')); + + for (var i = 0; i < posts.length; i++) { + let postNumber = posts[i].id.replace("pc", ""); + postNrs.push(postNumber); + } + debug(postNrs); +} + +function parseFoolFuukaPosts() { + let nums = x => x.filter(x => x.id !== '').map(x => x.id); + let getPostNumbers = x => nums(sliceCall(elementsInClass(x))); + + postNrs = getPostNumbers('thread').concat(getPostNumbers('post')); + debug(postNrs); +} + +function onFlagsLoad(response) { + + // because we only care about the end result, not how we got there. + let hopHTML = (post_nr, first, second) => + firstChildInClass(firstChildInClass(document.getElementById(post_nr), first), second); + + let MakeFlag = (flag) => createAndAssign('a', { + innerHTML: "", + className: "bantFlag", + target: "_blank" + }); + + debug("JSON: " + response.responseText); + var jsonData = JSON.parse(response.responseText); + + jsonData.forEach(function (post) { + debug(post); + let flagContainer = is_archive + ? hopHTML(post.post_nr, "post_data", "post_type") + : hopHTML("pc" + post.post_nr, "postInfo", "nameBlock"); + let currentFlag = firstChildInClass(flagContainer, 'flag'); + let postedRegions = post.region.split(regionDivider); + + // If we have a bantflag and the original post has a flag + if (postedRegions.length > 0 && currentFlag !== undefined) { + console.log("[BantFlags] Resolving flags for >>" + post.post_nr); + + for (var i = 0; i < postedRegions.length; i++) { + let flag = postedRegions[i]; + + let newFlag = MakeFlag(flag); + if (is_archive) { + newFlag.style = "padding: 0px 0px 0px " + (3 + 4 * (i > 0)) + "px; vertical-align:;display: inline-block; width: 16px; height: 11px; position: relative;"; + } + + flagContainer.append(newFlag); + + console.log("\t -> " + flag); + } + } + + // TODO: This can be postNrs.pop()? + // postNrs are resolved and should be removed from this variable + var index = postNrs.indexOf(post.post_nr); + if (index > -1) { + postNrs.splice(index, 1); + } + }); + + // TODO: do I need this? + // Removing posts older than the time limit (they likely won't resolve) + var timestampMinusPostRemoveCounter = Math.round(+new Date() / 1000) - postRemoveCounter; //should i remove this? + + postNrs.forEach(function (post_nr) { + let dateTime = is_archive + + // lol didn't expect to get to use this again + ? hopHTML(post_nr, 'post_data', 'time_wrap') + : hopHTML("pc" + post_nr, 'postInfo', 'dateTime'); + + if (dateTime.getAttribute("data-utc") < timestampMinusPostRemoveCounter) { + var index = postNrs.indexOf(post_nr); + if (index > -1) { + postNrs.splice(index, 1); + } + } + }); +} + +function resolveRefFlags() { + MakeRequest( + "POST", + back_end + api_get, + "post_nrs=" + encodeURIComponent(postNrs) + "&board=" + encodeURIComponent(boardID) + "&version=" + encodeURIComponent(version), + function (resp) { + if (resp.status !== 200) { + retry(resolveRefFlags, resp); + return; + } + onFlagsLoad(resp); + } + ); +} + +// Flags need to be parsed and aligned differently between boards. +if (is_archive) { + debug("FoolFuuka."); + parseFoolFuukaPosts(); + + addGlobalStyle('.bantFlag{top: -2px !important;left: -1px !important}'); +} +else { + debug("4chan."); + parse4chanPosts(); + + addGlobalStyle('.flag{top: 0px !important;left: -1px !important}'); + addGlobalStyle(".bantFlag {padding: 0px 0px 0px 5px; vertical-align:;display: inline-block; width: 16px; height: 11px; position: relative;}"); +} + +resolveRefFlags(); // Get flags from db. + +if (!is_archive) { + let GetEvDetail = e => e.detail || e.wrappedJSObject.detail; + + let method = "POST", + url = back_end + api_post, + func = function (resp) { + debug(resp.responseText); + }; + + // TODO: most of this can be rewritten. Do we care to support GM 1.x when we're using ES6? + /** send flag to system on 4chan x (v2, loadletter, v3 untested) post + * handy comment to save by ccd0 + * console.log(e.detail.boardID); // board name (string) + * console.log(e.detail.threadID); // thread number (integer in ccd0, string in loadletter) + * console.log(e.detail.postID); // post number (integer in ccd0, string in loadletter) */ + document.addEventListener('QRPostSuccessful', function (e) { + + //setTimeout to support greasemonkey 1.x + setTimeout(function () { + var data = "post_nr=" + encodeURIComponent(e.detail.postID) + "&board=" + encodeURIComponent(e.detail.boardID) + "®ions=" + encodeURIComponent(regions.slice().join(regionDivider)); + MakeRequest(method, url, data, func); + }, 0); + }, false); + + /** send flag to system on 4chan inline post */ + document.addEventListener('4chanQRPostSuccess', function (e) { + var evDetail = GetEvDetail(e); + + //setTimeout to support greasemonkey 1.x + setTimeout(function () { + var data = "post_nr=" + encodeURIComponent(evDetail.postId) + "&board=" + encodeURIComponent(boardID) + "®ions=" + encodeURIComponent(regions.slice().join(regionDivider)); + MakeRequest(method, url, data, func); + }, 0); + }, false); + + /** Listen to post updates from the thread updater for 4chan x v2 (loadletter) and v3 (ccd0 + ?) */ + document.addEventListener('ThreadUpdate', function (e) { + var evDetail = GetEvDetail(e); + var evDetailClone = typeof cloneInto === 'function' ? cloneInto(evDetail, unsafeWindow) : evDetail; + + //ignore if 404 event + if (evDetail[404] === true) { + return; + } + + setTimeout(function () { + + //add to temp posts and the DOM element to allPostsOnPage + evDetailClone.newPosts.forEach(function (post_board_nr) { + var post_nr = post_board_nr.split('.')[1]; + postNrs.push(post_nr); + }); + }, 0); + + //setTimeout to support greasemonkey 1.x + setTimeout(resolveRefFlags, 0); + }, false); + + /** Listen to post updates from the thread updater for inline extension */ + document.addEventListener('4chanThreadUpdated', function (e) { + var evDetail = GetEvDetail(e); + let threadID = window.location.pathname.split('/')[3]; + let postsContainer = sliceCall(document.getElementById('t' + threadID).childNodes); + let lastPosts = postsContainer.slice(Math.max(postsContainer.length - evDetail.count, 1)); //get the last n elements (where n is evDetail.count) + + //add to temp posts and the DOM element to allPostsOnPage + lastPosts.forEach(function (post_container) { + var post_nr = post_container.id.replace("pc", ""); + postNrs.push(post_nr); + }); + + //setTimeout to support greasemonkey 1.x + setTimeout(resolveRefFlags, 0); + }, false); + /** setup init and start first calls */ + nsetup.init(); +} \ No newline at end of file diff --git a/database.sql b/database.sql new file mode 100644 index 0000000..c3a7c98 --- /dev/null +++ b/database.sql @@ -0,0 +1,82 @@ +CREATE DATABASE IF NOT EXISTS bantflags; + +USE bantflags; + +CREATE USER IF NOT EXISTS flags@localhost IDENTIFIED BY 'default'; +GRANT ALL PRIVILEGES ON bantflags.* TO flags@localhost; +FLUSH PRIVILEGES; + +CREATE TABLE IF NOT EXISTS `flags` ( + `id` INT(10) NOT NULL AUTO_INCREMENT, + `flag` VARCHAR(100) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE INDEX `flag` (`flag`) +) +COLLATE='utf8_general_ci' +ENGINE=InnoDB +AUTO_INCREMENT=0 +; + +CREATE TABLE IF NOT EXISTS `posts` ( + `id` INT(10) NOT NULL AUTO_INCREMENT, + `post_nr` INT(10) NOT NULL DEFAULT '0', + `board` VARCHAR(5) NOT NULL DEFAULT 'bant', + PRIMARY KEY (`id`), + UNIQUE INDEX `post_nr` (`post_nr`) +) +COLLATE='utf8_general_ci' +ENGINE=InnoDB +AUTO_INCREMENT=0 +; + +CREATE TABLE IF NOT EXISTS `postflags` ( + `id` INT(10) NOT NULL AUTO_INCREMENT, + `post_nr` INT(10) NOT NULL DEFAULT '0', + `flag` INT(10) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + INDEX `flag` (`flag`), + INDEX `post_nr` (`post_nr`), + CONSTRAINT `flag` FOREIGN KEY (`flag`) REFERENCES `flags` (`id`), + CONSTRAINT `post_nr` FOREIGN KEY (`post_nr`) REFERENCES `posts` (`id`) +) +COLLATE='utf8_general_ci' +ENGINE=InnoDB +AUTO_INCREMENT=0 +; + +DROP PROCEDURE IF EXISTS insert_post; +DELIMITER $$ +CREATE DEFINER=`flags`@`localhost` PROCEDURE `insert_post`( + IN `@post_nr` INT, + IN `@board` VARCHAR(5) +) +LANGUAGE SQL +NOT DETERMINISTIC +CONTAINS SQL +SQL SECURITY DEFINER +COMMENT '' +BEGIN + INSERT IGNORE INTO `posts` (`post_nr`, `board`) VALUES (`@post_nr`, `@board`); +END +$$ +DELIMITER ; + +DROP PROCEDURE IF EXISTS insert_post_flags; +DELIMITER $$ +CREATE DEFINER=`flags`@`localhost` PROCEDURE `insert_post_flags`( + IN `@post_nr` INT, + IN `@flag` VARCHAR(100) +) +LANGUAGE SQL +NOT DETERMINISTIC +CONTAINS SQL +SQL SECURITY DEFINER +COMMENT '' +BEGIN +insert into postflags (post_nr, flag) VALUES ( +(select id from posts where post_nr = `@post_nr`), +(select id from flags where flag = `@flag`) +); +END +$$ +DELIMITER ;