Compare commits

..

14 Commits

Author SHA1 Message Date
not manx 2a5ac4b592
Use &aux for (conf)
4 years ago
not manx c4e13c4646
Fix CSS thing
4 years ago
not manx 878250343e
Improve update thread functions
4 years ago
not manx 50718e4ac5
Export *serb*
4 years ago
not manx 0adbfeed83
Uhh
4 years ago
not manx a479b49b3f
Lots of shit
4 years ago
not manx 65d43fd73d
regions was undefined if you had no flags set
5 years ago
not manx 497b87c9c3
Not setting post_nrs correctly
5 years ago
not manx 08254eda06
Make the connection pool work maybe?
5 years ago
not manx a31d24c28d
Moved hunchentoot utils to seperate package. Update readme
5 years ago
not manx 4214c213e3
Add license to top of each file, bump userscript version
5 years ago
not manx 220e660c84
Stop generating access logs
5 years ago
not manx e8198bd1e4
Move back to clsql
5 years ago
not manx a9e861eefd
Remove Ballmer
5 years ago

63
.gitattributes vendored

@ -1,63 +0,0 @@
###############################################################################
# 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

344
.gitignore vendored

@ -1,343 +1,3 @@
## 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
src/config.lisp src/config.lisp
\#*#
# User-specific files backups/
*.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/*.png
# 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

@ -1,25 +0,0 @@

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

@ -1,12 +0,0 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "3.1.0",
"commands": [
"dotnet-ef"
]
}
}
}

@ -1,24 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<Copyright>Manx</Copyright>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Magick.NET-Q8-AnyCPU" Version="7.14.5" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Razor" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.1.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
<PackageReference Include="MySql.Data" Version="8.0.18" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Nito.AsyncEx" Version="5.0.0" />
</ItemGroup>
</Project>

@ -1,84 +0,0 @@
// (C) Copyright 2019 C-xC-c <boku@plum.moe>
// This file is part of BantFlags.
// BantFlags is licensed under the GNU AGPL Version 3.0 or later.
// see the LICENSE file or <https://www.gnu.org/licenses/>
using BantFlags.Data;
using BantFlags.Data.Database;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
namespace BantFlags.Controllers
{
[ApiController]
[Route("api")]
public class FlagsController : Controller
{
private DatabaseService Database { get; }
public FlagsController(DatabaseService db)
{
Database = db;
}
/// <summary>
/// Retrives flags from the database from the posts sent in post_nrs
/// </summary>
/// <param name="post_nrs">The comma seperated list of post numbers from the thread.</param>
/// <param name="board">Currently should only be /bant/. Not checked here because we don't need to care what they send.</param>
/// <param name="version">The version of the userscript.</param>
[HttpPost]
[Route("get")]
[Consumes("application/x-www-form-urlencoded")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Get([FromForm]string post_nrs, [FromForm]string board, [FromForm]int? version)
{
int ver = version ?? 0;
if (ver > 1)
{
// Improved data structuring, see Docs/get
return Json(await Database.GetPosts_V2(post_nrs, board));
}
return Json(await Database.GetPosts_V1(post_nrs, board));
}
/// <summary>
/// Posts flags in the database.
/// </summary>
/// <param name="post_nr">The post number to associate the flags to.</param>
/// <param name="board">Currently should only be /bant/.</param>
/// <param name="regions">List of flags to associate with the post. Split by "||" in API V1 and "," in V2.</param>
/// <param name="version">The version of the userscript.</param>
[HttpPost]
[Route("post")]
[Consumes("application/x-www-form-urlencoded")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Post([FromForm]string post_nr, [FromForm]string board, [FromForm]string regions, [FromForm]int? version)
{
string splitFlag = (version ?? 0) > 1 ? "," : "||"; // comma for v2+, else || for backwards compatibility.
(PostModel flag, string error) = PostModel.Create(post_nr, board, regions, splitFlag, Database.KnownFlags, Database.Boards);
if (flag is null)
{
return Problem(error, statusCode: StatusCodes.Status400BadRequest);
}
await Database.InsertPost(flag);
return Ok(flag);
}
/// <summary>
/// Gets the list of supported flags.
/// </summary>
[HttpGet]
[Route("flags")]
[ProducesResponseType(StatusCodes.Status200OK)]
public IActionResult Flags() => Ok(Database.FlagList);
}
}

@ -1,115 +0,0 @@
// (C) Copyright 2019 C-xC-c <boku@plum.moe>
// This file is part of BantFlags.
// BantFlags is licensed under the GNU AGPL Version 3.0 or later.
// see the LICENSE file or <https://www.gnu.org/licenses/>
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
namespace BantFlags.Data.Database
{
/// <summary>
/// Functions for interacting with the database.
/// </summary>
public partial class DatabaseService
{
private MySqlConnectionPool ConnectionPool { get; }
public string FlagList { get; private set; }
public HashSet<string> KnownFlags { get; private set; }
public HashSet<string> Boards { get; private set; }
public DatabaseService(DatabaseServiceConfig dbConfig)
{
ConnectionPool = new MySqlConnectionPool(dbConfig.ConnectionString, dbConfig.PoolSize);
Boards = dbConfig.Boards;
UpdateKnownFlags().Wait(); // It's okay to deadlock here since it's only initialised at startup.
}
public async Task UpdateKnownFlags()
{
var flags = await GetFlags();
flags.Remove("empty, or there were errors. Re-set your flags."); // So users can't select this.
FlagList = string.Join("\n", flags);
KnownFlags = flags.ToHashSet();
}
public async Task InsertPost(PostModel 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));
}
}
}
/// <summary>
/// Returns all of the flags that we support.
/// </summary>
public async Task<List<string>> 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<string>("flag"))
.ToList();
}
public async Task InsertFlagAsync(Flag flag)
{
using var rentedConnection = await ConnectionPool.RentConnectionAsync();
await rentedConnection.Object.UseStoredProcedure("insert_flag")
.SetParam("@flag", flag.Name)
.ExecuteNonQueryAsync();
}
public async Task RenameFlagAsync(Flag flag)
{
using var rentedConnection = await ConnectionPool.RentConnectionAsync();
await rentedConnection.Object.UseStoredProcedure("rename_flag")
.SetParam("@old", flag.OldName)
.SetParam("@new", flag.Name)
.ExecuteNonQueryAsync();
}
public async Task DeleteFlagAsync(Flag flag)
{
using var rentedConnection = await ConnectionPool.RentConnectionAsync();
await rentedConnection.Object.UseStoredProcedure("delete_flag")
.SetParam("@flag", flag.Name)
.ExecuteNonQueryAsync();
}
}
/// <summary>
/// Configuration data passed by appsettings.
/// </summary>
public class DatabaseServiceConfig
{
public string ConnectionString { get; set; }
public int PoolSize { get; set; }
public HashSet<string> Boards { get; set; }
}
}

@ -1,58 +0,0 @@
// (C) Copyright 2019 C-xC-c <boku@plum.moe>
// This file is part of BantFlags.
// BantFlags is licensed under the GNU AGPL Version 3.0 or later.
// see the LICENSE file or <https://www.gnu.org/licenses/>
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
namespace BantFlags.Data.Database
{
public partial class DatabaseService
{
// Maybe this could be better but I don't know SQL lol
private readonly string GetPostsQuery = @"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)) AND posts.board = @board";
/// <summary>
/// Returns the post numbers and their flags from the post numbers in the input.
/// </summary>
/// <param name="post_nr">List of post numbers on the page.</param>
public async Task<IEnumerable<IGrouping<int, DataRow>>> GetPosts(string post_nr, string board)
{
using var rentedConnection = await ConnectionPool.RentConnectionAsync();
DataTable table = await rentedConnection.Object.CreateQuery(GetPostsQuery)
.SetParam("@posts", post_nr)
.SetParam("@board", board)
.ExecuteTableAsync();
return table.AsEnumerable()
.GroupBy(x => x.GetValue<int>("post_nr"));
}
public async Task<List<Dictionary<string, string>>> GetPosts_V1(string post_nr, string board)
{
List<Dictionary<string, string>> posts = new List<Dictionary<string, string>>();
var x = await GetPosts(post_nr, board);
x.ForEach(x => posts.Add(new Dictionary<string, string>
{
{"post_nr", x.Key.ToString() },
{"region", string.Join("||", x.AsEnumerable().Select(y => y.GetValue<string>("flag")))}
}));
return posts;
}
public async Task<Dictionary<int, IEnumerable<string>>> GetPosts_V2(string post_nr, string board)
{
var posts = await GetPosts(post_nr, board);
return posts
.ToDictionary(
x => x.Key,
x => x.AsEnumerable().Select(x => x.GetValue<string>("flag"))
);
}
}
}

@ -1,20 +0,0 @@
// (C) Copyright 2019 C-xC-c <boku@plum.moe>
// This file is part of BantFlags.
// BantFlags is licensed under the GNU AGPL Version 3.0 or later.
// see the LICENSE file or <https://www.gnu.org/licenses/>
using System;
using System.Collections.Generic;
namespace BantFlags.Data
{
public static class IEnumerableExtensions
{
public static void ForEach<T>(this IEnumerable<T> enumeration, Action<T> action)
{
foreach (T item in enumeration)
{
action(item);
}
}
}
}

@ -1,61 +0,0 @@
// (C) Copyright 2019 C-xC-c <boku@plum.moe>
// This file is part of BantFlags.
// BantFlags is licensed under the GNU AGPL Version 3.0 or later.
// see the LICENSE file or <https://www.gnu.org/licenses/>
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<MySqlConnection> Connections { get; }
protected string ConnectionString { get; }
protected int PoolSize { get; }
public MySqlConnectionPool(string connectionString, int poolSize)
{
PoolSize = poolSize;
ConnectionString = connectionString;
Connections = new AsyncCollection<MySqlConnection>(poolSize);
for (int i = 0; i < poolSize; i++)
{
var connection = new MySqlConnection(connectionString);
connection.Open();
Connections.Add(connection);
}
}
public async Task<PoolObject<MySqlConnection>> RentConnectionAsync()
{
return new PoolObject<MySqlConnection>(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();
}
}
}
}

@ -1,48 +0,0 @@
// (C) Copyright 2019 C-xC-c <boku@plum.moe>
// This file is part of BantFlags.
// BantFlags is licensed under the GNU AGPL Version 3.0 or later.
// see the LICENSE file or <https://www.gnu.org/licenses/>
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<T>(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();
}
}
}
}

@ -1,29 +0,0 @@
// (C) Copyright 2019 C-xC-c <boku@plum.moe>
// This file is part of BantFlags.
// BantFlags is licensed under the GNU AGPL Version 3.0 or later.
// see the LICENSE file or <https://www.gnu.org/licenses/>
using System;
namespace BantFlags.Data
{
public class PoolObject<T> : IDisposable
{
public T Object { get; }
private Action<T> ReturnAction { get; }
public PoolObject(T o, Action<T> returnAction)
{
Object = o;
ReturnAction = returnAction;
}
public void Dispose()
{
ReturnAction(Object);
}
public static implicit operator T(PoolObject<T> poolObject)
=> poolObject.Object;
}
}

@ -1,52 +0,0 @@
// (C) Copyright 2019 C-xC-c <boku@plum.moe>
// This file is part of BantFlags.
// BantFlags is licensed under the GNU AGPL Version 3.0 or later.
// see the LICENSE file or <https://www.gnu.org/licenses/>
using System.Collections.Generic;
using System.Linq;
namespace BantFlags.Data
{
public class PostModel
{
public int PostNumber { get; private set; }
public string Board { get; private set; }
public string[] Flags { get; private set; }
private PostModel(int post_nr, string board, string[] flags)
{
PostNumber = post_nr;
Board = board;
Flags = flags;
}
public static (PostModel, string) Create(string post_nr, string board, string regions, string splitFlag, HashSet<string> knownFlags, HashSet<string> boards)
{
string[] empty = { "empty, or there were errors. Re-set your flags." };
if (!int.TryParse(post_nr, out int postNumber))
return (default, "Invalid post number.");
if (!boards.Contains(board))
return (default, "Invalid board parameter.");
if (regions == null)
return (new PostModel(postNumber, board, empty), default);
var flags = regions.Split(splitFlag);
if (flags.Count() > 30)
return (default, "Too many flags.");
foreach (string flag in flags)
{
if (!knownFlags.Contains(flag)) // Not ideal but it's better than doing it in the controller or passing the database here.
return (new PostModel(postNumber, board, empty), default);
}
return (new PostModel(postNumber, board, flags), default);
}
}
}

@ -1,65 +0,0 @@
// (C) Copyright 2019 C-xC-c <boku@plum.moe>
// This file is part of BantFlags.
// BantFlags is licensed under the GNU AGPL Version 3.0 or later.
// see the LICENSE file or <https://www.gnu.org/licenses/>
using MySql.Data.MySqlClient;
using System;
using System.Data;
using System.Threading.Tasks;
namespace BantFlags.Data
{
/// <summary>
/// Succinct methods for creating and executing database queries
/// </summary>
public class Query : IDisposable
{
private MySqlCommand Command { get; }
public Query(MySqlCommand cmd)
{
Command = cmd;
}
public async Task<DataTable> 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();
}
}
}

@ -1,150 +0,0 @@
// (C) Copyright 2019 C-xC-c <boku@plum.moe>
// This file is part of BantFlags.
// BantFlags is licensed under the GNU AGPL Version 3.0 or later.
// see the LICENSE file or <https://www.gnu.org/licenses/>
using ImageMagick;
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace BantFlags.Data
{
public class Staging
{
public List<Flag> Flags { get; set; }
public string Password { get; }
public HashSet<string> Names { get; set; }
public Staging(string password)
{
Flags = new List<Flag>();
Password = password;
}
public void Clear()
{
Flags = new List<Flag>();
}
}
public enum Method
{
Add = 0,
Delete = 1,
Rename = 2
}
public class Flag
{
public string Name { get; set; }
public string OldName { get; set; }
public bool IsChecked { get; set; }
public Method FlagMethod { get; set; }
// This is bad but we need it so Flags can be generated by the input tag helper
public Flag()
{
}
private Flag(string name, Method method)
{
Name = name;
FlagMethod = method;
}
private Flag(string name, string oldName, Method method)
{
Name = name;
OldName = oldName;
FlagMethod = method;
}
public static Flag CreateFromDelete(string name) => new Flag(name, Method.Delete); // We don't need any validation for deleted flags.
public static (Flag, string) CreateFromRename(string oldName, string newName, HashSet<string> names)
{
(bool valid, string error) = ValidateFileName(newName, names);
if (!valid)
{
return (default, error);
}
return (new Flag(newName, oldName, Method.Rename), default);
}
public static async Task<(Flag, string)> CreateFromFile(IFormFile upload, HashSet<string> names)
{
byte[] PNGHeader = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
if (upload.ContentType.ToLower() != "image/png")
return (default, "Image must be a png.");
if (upload.Length > 15 * 1024)
return (default, "File too big. Max size is 15kb.");
var name = Path.GetFileNameWithoutExtension(upload.FileName);
(bool valid, string error) = ValidateFileName(name, names);
if (!valid)
return (default, error);
using (var memoryStream = new MemoryStream())
{
await upload.CopyToAsync(memoryStream);
memoryStream.Position = 0;
using (var image = new MagickImage(memoryStream))
{
if (image.Width != 16 || image.Height != 11)
return (default, "Invalid image dimensions. Flags should be 16px by 11px.");
}
using (var reader = new BinaryReader(memoryStream))
{
reader.BaseStream.Position = 0;
if (!reader.ReadBytes(PNGHeader.Length).SequenceEqual(PNGHeader))
return (default, "Invalid png header.");
}
}
return (new Flag(name, Method.Add), default);
}
/// <summary>
/// Filters file names created by users.
/// </summary>
/// <param name="name">The file name to validate.</param>
/// <param name="names">The list of current file names.</param>
private static (bool, string) ValidateFileName(string name, HashSet<string> names)
{
if (string.IsNullOrWhiteSpace(name))
return (false, "Flag name can't be empty.");
if (name.Length > 100)
return (false, "Flag name too long.");
if (name == "empty, or there were errors. Re - set your flags.")
return (false, "Invalid flag name.");
if (name.Contains("||") || name.Contains(","))
return (false, "Flag name contains invalid characters. You can't use \"||\" or \",\".");
if (names.Contains(name))
return (false, "A flag with that name already exists.");
return (true, name);
}
}
}

@ -1,21 +0,0 @@
@* (C) Copyright 2019 C-xC-c <boku@plum.moe>
This file is part of BantFlags.
BantFlags is licensed under the GNU AGPL Version 3.0 or later.
see the LICENSE file or <https: //www.gnu.org/licenses/agpl-3.0.en.html /> *@
@page
@model BantFlags.Pages.IndexModel
@{
ViewData["Title"] = "BantFlags";
Layout = "~/Pages/Shared/_Layout.cshtml";
}
<h1>/bant/ Flags</h1>
<p>E<span lang="jp">メール</span>: boku (at) plum (dot) moe</p>
<a href="~/bantflags.user.js">Install Bantflags</a>
<br />
<br />
<a href="https://nineball.party/srsbsn/3521#bottom">Official Thread</a>
<br />
<br />
<a asp-page="Upload">Upload Flags</a>

@ -1,15 +0,0 @@
// (C) Copyright 2019 C-xC-c <boku@plum.moe>
// This file is part of BantFlags.
// BantFlags is licensed under the GNU AGPL Version 3.0 or later.
// see the LICENSE file or <https://www.gnu.org/licenses/>
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace BantFlags.Pages
{
public class IndexModel : PageModel
{
public void OnGet()
{
}
}
}

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"]</title>
@RenderSection("Head", required: false)
</head>
<body style="text-align: center;">
@RenderBody()
@RenderSection("Scripts", required: false)
</body>
</html>

@ -1,123 +0,0 @@
@* (C) Copyright 2019 C-xC-c <boku@plum.moe>
This file is part of BantFlags.
BantFlags is licensed under the GNU AGPL Version 3.0 or later.
see the LICENSE file or <https: //www.gnu.org/licenses/agpl-3.0.en.html /> *@
@page
@using BantFlags.Data
@model BantFlags.UploadModel
@{
ViewData["Title"] = "Upload";
Layout = "~/Pages/Shared/_Layout.cshtml";
}
<h1>Upload</h1>
<img src="~/montage.png" />
<h2 id="message">@Model.Message</h2>
<h2>Add a Flag</h2>
<form method="post" asp-page-handler="Add" enctype="multipart/form-data">
<input type="file" name="upload" />
<input type="checkbox" name="gloss" value="true" /><span>Apply Gloss?</span>
<br />
<br />
<input type="submit" value="Upload Flag" />
</form>
<h2>Manage Existing Flags</h2>
<form method="post">
<select name="flag">
@foreach (string s in Model.StagedFlags.Names)
{
<option>@s</option>
}
</select>
<input type="text" name="newName" />
<br />
<br />
<button asp-page-handler="Delete" type="submit">Delete Flag</button>
<button asp-page-handler="Rename" type="submit">Rename Flag</button>
</form>
<h2>Commit Changes</h2>
<form method="post" asp-page-handler="Commit" enctype="multipart/form-data">
<label>Password:</label>
<input type="text" name="password" />
<br />
<br />
<input type="submit" value="Commit Changes" />
</form>
@if (Model.StagedFlags.Flags.Any())
{
<h2>Pending Changes</h2>
<form method="post" asp-page-handler="Unstage">
<input type="text" name="password" />
<br />
<br />
<button type="submit">Remove from staging</button>
<h3>Deleted Flags</h3>
@for (int i = 0; i < Model.StagedFlags.Flags.Count(); i++)
{
if (Model.StagedFlags.Flags[i].FlagMethod == Method.Delete)
{
<div class="flag">
<label>@(Model.StagedFlags.Flags[i].Name)</label>
<img src="~/flags/@(Model.StagedFlags.Flags[i].Name).png" />
<input type="hidden" name="flags[@i].Name" value="@Model.StagedFlags.Flags[i].Name" />
<input type="hidden" name="flags[@i].FlagMethod" value="@Model.StagedFlags.Flags[i].FlagMethod" />
<input type="checkbox" name="flags[@i].IsChecked" value="true" />
</div>
}
}
<h3>Renamed Flags</h3>
@for (int i = 0; i < Model.StagedFlags.Flags.Count(); i++)
{
if (Model.StagedFlags.Flags[i].FlagMethod == Method.Rename)
{
<div class="flag">
<label>@(Model.StagedFlags.Flags[i].Name)</label>
<img src="~/flags/@(Model.StagedFlags.Flags[i].OldName).png" />
<input type="hidden" name="flags[@i].Name" value="@Model.StagedFlags.Flags[i].Name" />
<input type="hidden" name="flags[@i].FlagMethod" value="@Model.StagedFlags.Flags[i].FlagMethod" />
<input type="checkbox" name="flags[@i].IsChecked" value="true" />
</div>
}
}
<h3>Added Flags</h3>
@for (int i = 0; i < Model.StagedFlags.Flags.Count(); i++)
{
if (Model.StagedFlags.Flags[i].FlagMethod == Method.Add)
{
<div class="flag">
<label>@(Model.StagedFlags.Flags[i].Name)</label>
<img src="~/flags/staging/@(Model.StagedFlags.Flags[i].Name).png" />
<input type="hidden" name="flags[@i].Name" value="@Model.StagedFlags.Flags[i].Name" />
<input type="hidden" name="flags[@i].FlagMethod" value="@Model.StagedFlags.Flags[i].FlagMethod" />
<input type="checkbox" name="flags[@i].IsChecked" value="true" />
</div>
}
}
</form>
}
@section Head {
<link rel="stylesheet" href="~/upload.css" />
}
@section Scripts {
@* Place flag image inside the <select> because ASP removes "invalid HTML" *@
<script>
window.addEventListener('load', function (e) {
let x = document.getElementsByTagName('select')[0].children
Array.prototype.slice.call(x).forEach(function (y) {
var name = y.innerHTML;
y.innerHTML = "<img src=\"flags/" + name + ".png\">" + name
});
}, { once: true });
</script>
}

@ -1,191 +0,0 @@
// (C) Copyright 2019 C-xC-c <boku@plum.moe>
// This file is part of BantFlags.
// BantFlags is licensed under the GNU AGPL Version 3.0 or later.
// see the LICENSE file or <https://www.gnu.org/licenses/>
using BantFlags.Data;
using BantFlags.Data.Database;
using ImageMagick;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace BantFlags
{
public class UploadModel : PageModel
{
private DatabaseService Database { get; set; }
public Staging StagedFlags { get; set; }
public string Message { get; private set; }
private string WebRoot { get; }
public HashSet<string> AllNames => StagedFlags.Names.Concat(StagedFlags.Flags.Select(x => x.Name)).ToHashSet();
public UploadModel(DatabaseService dbs, Staging ns, IWebHostEnvironment env)
{
Database = dbs;
StagedFlags = ns;
WebRoot = env.WebRootPath;
}
public void OnGet()
{
StagedFlags.Names = StagedFlags.Names ?? Database.KnownFlags;
}
public IActionResult OnPostDelete(string flag)
{
var stagingFlag = Flag.CreateFromDelete(flag);
StagedFlags.Flags.Add(stagingFlag);
StagedFlags.Names.Remove(stagingFlag.Name);
Message = $"{stagingFlag.Name} deleted.";
return Page();
}
public IActionResult OnPostRename(string flag, string newName)
{
(Flag stagingFlag, string error) = Flag.CreateFromRename(flag, newName, AllNames);
if (stagingFlag is null)
{
Message = error;
return Page();
}
StagedFlags.Flags.Add(stagingFlag);
StagedFlags.Names.Add(stagingFlag.Name);
Message = $"{stagingFlag.OldName} renamed to {stagingFlag.Name}.";
return Page();
}
public async Task<IActionResult> OnPostAddAsync(IFormFile upload, bool gloss)
{
(Flag stagingFlag, string error) = await Flag.CreateFromFile(upload, AllNames);
if (stagingFlag is null)
{
Message = error;
return Page();
}
using var memoryStream = new MemoryStream();
await upload.CopyToAsync(memoryStream);
memoryStream.Position = 0;
// Magic.NET is a huge dependency to be used like this
// Maybe we should switch to a Process and expect to have
// ImageMagick installed on the target machine.
using var image = new MagickImage(memoryStream);
if (gloss)
{
using var glossImage = new MagickImage(WebRoot + "/gloss.png");
glossImage.Composite(image, new PointD(0, 0), CompositeOperator.Over);
}
image.Write(WebRoot + "/flags/staging/" + upload.FileName);
StagedFlags.Flags.Add(stagingFlag);
Message = $"{stagingFlag.Name} uploaded";
return Page();
}
public IActionResult OnPostUnstage(Flag[] flags, string password)
{
if (password != StagedFlags.Password)
{
Message = "Incorrect Password";
return Page();
}
for (int i = flags.Length - 1; i >= 0; i--)
{
if (flags[i].IsChecked != true)
{
continue;
}
StagedFlags.Flags.RemoveAt(i);
var flag = flags[i];
switch (flag.FlagMethod)
{
case Method.Add:
System.IO.File.Delete(WebRoot + "/flags/staging/" + flag.Name);
StagedFlags.Names.Remove(flag.Name);
break;
case Method.Delete:
StagedFlags.Names.Add(flag.Name);
break;
case Method.Rename:
StagedFlags.Names.Remove(flag.Name);
break;
default:
throw new Exception();
}
}
Message = "Removed flags from staging";
return Page();
}
public async Task<IActionResult> OnPostCommit(string password)
{
if (password != StagedFlags.Password)
{
Message = "Incorrect Password";
return Page();
}
foreach (var flag in StagedFlags.Flags)
{
string flagname = flag.Name + ".png";
switch (flag.FlagMethod)
{
case Method.Add:
await Database.InsertFlagAsync(flag);
Directory.Move(WebRoot + "/flags/staging/" + flagname, WebRoot + "/flags/" + flagname);
break;
case Method.Delete:
await Database.DeleteFlagAsync(flag);
Directory.Move(WebRoot + "/flags/" + flagname, WebRoot + "/flags/dead/" + Guid.NewGuid().ToString()); // Use a GUID so flags with the same name can be deleted.
break;
case Method.Rename:
await Database.RenameFlagAsync(flag);
Directory.Move(WebRoot + "/flags/" + flag.OldName + ".png", WebRoot + "/flags/" + flagname);
break;
default:
throw new Exception();
}
}
await Database.UpdateKnownFlags();
StagedFlags.Names = Database.KnownFlags;
StagedFlags.Clear();
Message = "Changes committed successfully";
return Page();
}
}
}

@ -1,3 +0,0 @@
@using BantFlags
@namespace BantFlags.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

@ -1,3 +0,0 @@
@{
Layout = "_Layout";
}

@ -1,31 +0,0 @@
// (C) Copyright 2019 C-xC-c <boku@plum.moe>
// This file is part of BantFlags.
// BantFlags is licensed under the GNU AGPL Version 3.0 or later.
// see the LICENSE file or <https://www.gnu.org/licenses/>
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
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 =>
{
webBuilder.UseStartup<Startup>();
})
.ConfigureAppConfiguration((host, config) =>
{
// Explicitly look for appsettings.json in the program's directory
config.AddJsonFile(Path.Join(System.AppDomain.CurrentDomain.BaseDirectory + "appsettings.json"), optional: false, reloadOnChange: false);
});
}
}

@ -1,30 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:17777",
"sslPort": 44366
}
},
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "weatherforecast",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"BantFlags": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "weatherforecast",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}

@ -1,83 +0,0 @@
// (C) Copyright 2019 C-xC-c <boku@plum.moe>
// This file is part of BantFlags.
// BantFlags is licensed under the GNU AGPL Version 3.0 or later.
// see the LICENSE file or <https://www.gnu.org/licenses/>
using BantFlags.Data;
using BantFlags.Data.Database;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using System.IO;
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(); // So we can format complex datatypes into JSON
services.AddRazorPages();
if (!Directory.Exists(Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "keys/")))
{
Directory.CreateDirectory(Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "keys/"));
}
services.AddDataProtection()
.SetApplicationName("BantFlags")
.PersistKeysToFileSystem(new DirectoryInfo(Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "keys")));
services.AddSingleton(new DatabaseService(Configuration.GetSection("dbconfig").Get<DatabaseServiceConfig>()));
services.AddSingleton(new Staging(Configuration.GetValue<string>("staging-password")));
}
// 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<string>("webroot");
if (Directory.Exists(webroot))
{
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.MapRazorPages();
endpoints.MapControllers();
});
}
}
}

@ -1,14 +0,0 @@
{
"AllowedHosts": "*",
"dbconfig": {
"connectionstring": "Server=localhost;Port=3306;User ID=user;Password=password;Database=bantflags",
"poolsize": 2,
"boards": [
"bant",
"nap",
"srsbsn"
]
},
"webroot": "/var/www/html",
"staging-password": "supersecretpassword"
}

@ -1,4 +0,0 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

@ -1,4 +0,0 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

Binary file not shown.

Before

Width:  |  Height:  |  Size: 436 B

@ -1,28 +0,0 @@
body {
background: #ffe url(fade.png) top center repeat-x;
}
h1, h2, h3 {
color: maroon;
}
h3 {
clear: both;
padding-top: 20px;
}
.flag-container {
width: 60%;
margin: auto;
}
.flag {
margin: auto;
width: 12%;
display: inline-block;
padding-bottom: 10px;
}
#message {
color: #ee0;
}

@ -1,4 +0,0 @@
* /api/flags
returns a plaintext list of all the resolved flags, seperated by
newline characters (\n). This is maintained by the database service
and updated whenever changes are committed to the flag console.

@ -1,70 +0,0 @@
* /api/get
** Recieved Data
All versions of the script send a comma seperated list of post
numbers, =post_nrs= and a board =board=. Modern versions of bantflags
also send a version number, =version=, which decides how the returned
data will be formatted.
Data looks like this:
#+BEGIN_SRC http
POST https://flags.plum.moe/api/get
Content-Type: application/x-www-form-urlencoded
post_nrs=12345,12346,14444&board=bant&version=1
#+END_SRC
** Sent Data
*** GetPosts
Returns an =IEnumerable<IGrouping<int, DataRow>>= of post numbers and
their flags where the post numbers are contained in
=post_nrs=. =board= limits the query to only the board we're currently
on.
*** GetPosts_V1
Minimum script version: 0
Flags are converted from an =IEnumerable<IGrouping<int, DataRow>>= to
a =List<Dictionary<string, string>>= by joining the values in the
=DataRow= by "||", which are then split and converted into an array by
the script.
We're doing a needless conversion at both ends which slows the whole
process down, but it's how extraflags is set up and we need to support
it.
Data looks like this:
#+BEGIN_SRC javascript
[
{
{"post_nr": "123"},
{"regions": "flag1||flag2||flag3"}
},
{
{"post_nr": "456"},
{"regions": "flag4||flag3||flag3"}
}
]
#+END_SRC
*** getPosts_V2
Minimum script version: 2
Flags are converted from an =IEnumerable<IGrouping<int, DataRow>>= to
a =Dictionary<int, IEnumerable<string>>= which can then be parsed by
the script without any conversion. This format is the same as returned
from the database query, sans the extra information returned by a
=DataRow=
Data looks like this:
#+BEGIN_SRC javascript
[
123: [
"flag1",
"flag2",
"flag3"
],
456: [
"flag4",
"flag3",
"flag3"
]
]
#+END_SRC

@ -1,18 +0,0 @@
* /api/post
** Recieved Data
Data is sent from the script to the backend after a post has been
made, containing the post number, =post_nr=, =board= identifier, and
selected flags, =regions=. Modern versions of the script also encode a
=version= which is used when splitting =regions= into individual flags
Data looks like this:
#+BEGIN_SRC http :pretty
POST https://flags/plum.moe/api/post
Content-Type: application/x-www-form-urlencoded
post_nr=12345&board=bant&regions=Patchouli,dount,VIP&version=1
#+END_SRC
** Sent Data
The backend returns either a JSON object of the post, as it would be
returned by =/api/get=, or an error message specific to the issue if
the form data is invalid.

@ -1,15 +0,0 @@
* /Upload
The flag console is the preferred way for adding and modifying flags
in the database. It provides controls for adding, deleting and
renaming flags, all of which are visible before being committed by
someone with with password, which is set using the =staging-password=
section in =appsettings.json=. Changes may be unstaged on an
individual basis, but committed changes must be done all at once.
Flags have several hard-coded requirements for being uploaded using
the console. The image must be 16 by 11 pixels in size, be a PNG file
and have a valid PNG header. Names of files must be under 100
characters in length, and not contain either "," or
"||". Additionally, you cannot name a flag "Empty, or there were
errors. Please re-set your flags." as this is used, well, when someone
hasn't set their flags right.

@ -1,102 +0,0 @@
DROP DATABASE IF EXISTS `bantflags`;
CREATE DATABASE `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 `flags` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`flag` varchar(100) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `flag` (`flag`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
CREATE TABLE `posts` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`post_nr` int(10) NOT NULL DEFAULT '0',
`board` varchar(10) NOT NULL DEFAULT 'bant',
PRIMARY KEY (`id`),
UNIQUE KEY `post_nr_board` (`post_nr`,`board`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
CREATE TABLE `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`),
KEY `flag` (`flag`),
KEY `post_nr` (`post_nr`),
CONSTRAINT `flag` FOREIGN KEY (`flag`) REFERENCES `flags` (`id`) ON DELETE CASCADE,
CONSTRAINT `post_nr` FOREIGN KEY (`post_nr`) REFERENCES `posts` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
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
BEGIN
INSERT IGNORE INTO `posts` (`post_nr`, `board`) VALUES (`@post_nr`, `@board`);
END
$$
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
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
$$
CREATE DEFINER=`flags`@`localhost` PROCEDURE `rename_flag`(
IN `@old` VARCHAR(100),
IN `@new` VARCHAR(100)
)
LANGUAGE SQL
NOT DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
BEGIN
UPDATE flags SET flags.flag = `@new` WHERE flags.flag = `@old`;
END
$$
CREATE DEFINER=`flags`@`localhost` PROCEDURE `delete_flag`(
IN `@flag` VARCHAR(100)
)
LANGUAGE SQL
NOT DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
BEGIN
DELETE flags.* FROM flags WHERE flags.flag = `@flag`;
END
$$
CREATE DEFINER=`flags`@`localhost` PROCEDURE `insert_flag`(
IN `@flag` VARCHAR(100)
)
LANGUAGE SQL
NOT DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
BEGIN
INSERT INTO `flags` (`flag`) VALUES (`@flag`);
END
$$
DELIMITER ;

@ -1,118 +1,93 @@
* DotnetFlags
This branch exists as an archive of bantflags before it was rewritten
in Common Lisp. The code here still works but is unmaintained.
* BantFlags * BantFlags
A user script and backend enabling user created flags on [[https://boards.4chan.org/bant][/bant/]], A user script and backend enabling user created flags on [[https://boards.4chan.org/bant][/bant/]],
originally based on [[https://github.com/flaghunters/Extra-Flags-for-4chan][extraflags]]. originally based on [[https://github.com/flaghunters/Extra-Flags-for-4chan][extraflags]].
[[https://flags.plum.moe/bantflags.user.js][Install bantflags]] [[https://flags.plum.moe/bantflags.user.js][Install bantflags]]
** Userscript ** Userscript
The userscript uses of ~GM_xmlhttpRequest~ to get and post flags with The userscript uses of ~GM_xmlhttpRequest~ to get and post flags
the backend . A user's flags are stored between pages using with the backend . A user's flags are stored between pages using
~GM_setValue~ and ~GM_getValue~. ~GM_setValue~ and ~GM_getValue~, or their GreaseMonkey4
equivalents.
Old versions of GreaseMonkey will be able to recieve updates to the Old versions of GreaseMonkey will be able to recieve updates to the
script through the ~@updateURL~ and ~@downloadURL~ directives, though script through the ~@updateURL~ and ~@downloadURL~ directives,
these were depricated sometime in GreaseMonkey 3.x and updates are though these were depricated sometime in GreaseMonkey 3.x and
only checked from the location the script was downloaded from so be updates are only checked from the location the script was
careful where you upload links. downloaded from so be careful where you upload links.
On self hosting, changing ~back_end~ to your domain /should/ be all On self hosting, changing ~back_end~ to your domain /should/ be all
you need to do, but don't take this as fact. I haven't tested the you need to do, but don't take this as fact.
example nginx config.
The userscript has been designed specifically to target ECMAScript
The userscript has been designed specifically to target ECMAScript 2015 (ES6), making liberal use of arrow functions, and const/let
2015 (ES6), making liberal use of arrow functions, and const/let declarations. Update your hecking browser.
declarations. Update your hecking browser.
** Backend ** Backend
*** Prerequisites *** Prerequisites
- .NET Core 3.1 - I use SBCL
- MariaDB / MySQL - Some mysql, I use Mariadb
*** .NET dependancies - Quicklisp
- Nito.AsyncEX *** Dependancies
- Newtonsoft.Json - hunchentoot
- MySql.Data - [[https://github.com/C-xC-c/hunchenhelpers][hunchenhelpers]], my hunchentoot helper library (yes I'm proud of
- Microsoft.AspNetCore.Mvc.NewtonsoftJson the name)
- Microsoft.AspNetCore.StaticFiles - clsql
- Microsoft.AspNetCore.Razor - jonathan, the JSON encoder/decoder
- Microsoft.EntityFrameworkCore.SqlServer - cl-ppcre
- Microsoft.EntityFrameworkCore.Tools
- Magick.NET-Q8-AnyCPU
*** Setup *** Setup
1) [[https://dotnet.microsoft.com/download/dotnet-core][Install .NET Core]] 1. clone the project
2) Clone and build the BantFlags solution. 2. Symlink src/ to your ~/quicklisp/local-projects
3) Create the database using [[https://github.com/C-xC-c/BantFlags/blob/master/Environment/database.sql][database.sql]]. 3. Move ~src/config.example.org~ to ~src/config.org~ and change it
- *Change the password*. to whatever your settings are.
4) configure ~BantFlags/appsettings.example.json~ with your connection 4. Initialise the database by doing something like ~mysql <<
string and webroot (where you'll serve the flags from *without a env/database.sql~, This will create all the tables you will
trailing slash*) and rename it to ~appsettings.json~ need, plus an entry for the ~`empty flag`~
- [[./BantFlags/appsettings.example.json][example appsettings.json]] 5. Type the following into your repl:
- ASP.NET Core applications look for a folder called ~wwwroot~ in #+BEGIN_SRC lisp
the same directory as the application for static files. However (ql:quickload :bantflags)
you can choose to logically seperate these by providing a vaild (bantflags:main)
directory to ~webroot~. #+END_SRC
- That is to say, if the bantflags application is in 6. To use bantflags as a Systemd service, I have included an
~/var/www/bantflags/BantFlags.dll~, the program will look for example service and an ~init.el~ file for the service to run,
the folder ~/var/www/bantflags/wwwroot/~ to host static content, since Systemd will automatically kill it if you just eval
or whatever directory is provided to ~wwwroot~. ~bantflags:main~.
5) If you're hosting on your GNU/Linux distribution of choice, Create a You will almost certainly have several issues building clsql, the
folder called ~keys~ in the same directory as the bantflags database connector used. I've [[https://plum.moe/words/bludgeoning-clsql-and-mariadb.html][written a blog post]] on some of the
executable. issues I've encountered personally, but there's no guarantee it'll
- E.G. ~/var/www/bantflags/keys/~ work. Piece of shit.
- This is because ASP.NET Core uses some cryptic bullshit anti
forgery token when processing HTML forms, and it's unable to
persistantly store the decryption keys in memory on
GNU/Linux. This directory will store said keys when you or
users upload flags to /upload. The path uses
~AppDomain.CurrentDomain.BaseDirectory~ internally,
I.E. wherever the program is.
6) Add flags to the backend by uploading them to the flag console (/Upload).
- Flags must be 16x11 pixels and under 15kb. Their names must not
exceed 100 characters and cannot contain either "||" or ",".
7) Configure your webserver of choice to forward requests to kestral
- [[https://github.com/C-xC-c/BantFlags/blob/master/Environment/nginx.conf][Example nginx config.]]
8) Run with ~dotnet BantFlags.dll~ or create a service to run it as a
daemon.
- [[https://github.com/C-xC-c/BantFlags/blob/master/Environment/bantflags.service][Example systemd service.]]
9) ???
10) profit.
*** Database *** Database
Tables look like this: Tables look like this:
*posts* *posts*
| id | post_nr | board | | id | post_nr | board |
| 1 | 12345 | bant | | 1 | 12345 | bant |
| 2 | 56789 | bant | | 2 | 56789 | bant |
*flags* *flags*
| id | flag | | id | flag |
| 1 | patchouli | | 1 | patchouli |
| 2 | chen | | 2 | chen |
*postflags* *postflags*
| id | post_nr | flag | | id | post_nr | flag |
| 1 | 1 | 1 | | 1 | 1 | 1 |
| 2 | 1 | 2 | | 2 | 1 | 2 |
| 2 | 2 | 2 | | 2 | 2 | 2 |
where ~post_nr~ and ~flag~ in *postflags* are the id fields in their where ~post_nr~ and ~flag~ in *postflags* are the id fields in their
respective tables. respective tables.
*** API *** API
The backend exposes three endpoints for the userscript to get and post The backend exposes three endpoints for the userscript to get and
flags. Flags themselves are hosted from the ~flags/~ directory. This post flags. Flags themselves are hosted from ~flags/~ which is
will be whatever value you gave to ~webroot~ (or ~www-root/flags/~ from ~config.lisp~ on the filesystem
~/path/to/bantflags/wwwroot/~ if no value is provided) + ~flags/~.
| route | purpse |
|------------+--------------------------------------------|
| /api/get | Get flags using post numbers in the thread |
| /api/post | Add flags to the database |
| /api/flags | List the flags we support |
| /flags/* | The flag images |
** Backwards Compatibility | route | purpse |
The API is 1:1 compatable with all previous versions of |------------+--------------------------------------------|
bantflags. Further improvements are achieved by encoding a ~version~ | /api/get | Get flags using post numbers in the thread |
variable when poking endpoints which allows for breaking changes in | /api/post | Add flags to the database |
the script and backend while guaranteeing data can be parsed on both | /api/flags | List the flags we support |
ends. See [[https://github.com/C-xC-c/BantFlags/tree/master/Docs/][Docs/{endpoint}]] for changes and compatibility. | /flags/* | The flag images |
** Notes
You will get an error like =Recursive lock attempt #<SB-THREAD:MUTEX
"global-message-log-lock" owner: #<SB-THREAD:THREAD
"hunchentoot-worker-127.0.0.1:54454" RUNNING {1001DED5E3}>>.= if you
try and log to a file that doesn't exist / you don't have permissions
to read/write.

@ -11,7 +11,7 @@
// @exclude http*://archive.nyafuu.org/bant/statistics/ // @exclude http*://archive.nyafuu.org/bant/statistics/
// @exclude http*://archived.moe/bant/statistics/ // @exclude http*://archived.moe/bant/statistics/
// @exclude http*://thebarchive.com/bant/statistics/ // @exclude http*://thebarchive.com/bant/statistics/
// @version 1.5.2 // @version 2.1.0
// @grant GM_xmlhttpRequest // @grant GM_xmlhttpRequest
// @grant GM_getValue // @grant GM_getValue
// @grant GM_setValue // @grant GM_setValue

@ -11,7 +11,7 @@
// @exclude http*://archive.nyafuu.org/bant/statistics/ // @exclude http*://archive.nyafuu.org/bant/statistics/
// @exclude http*://archived.moe/bant/statistics/ // @exclude http*://archived.moe/bant/statistics/
// @exclude http*://thebarchive.com/bant/statistics/ // @exclude http*://thebarchive.com/bant/statistics/
// @version 1.5.2 // @version 2.1.0
// @grant GM_xmlhttpRequest // @grant GM_xmlhttpRequest
// @grant GM_getValue // @grant GM_getValue
// @grant GM_setValue // @grant GM_setValue
@ -29,17 +29,14 @@
// /bant/ Flags is licensed under the GNU AGPL Version 3.0 or later. // /bant/ Flags is licensed under the GNU AGPL Version 3.0 or later.
// see the LICENSE file or <https://www.gnu.org/licenses/> // see the LICENSE file or <https://www.gnu.org/licenses/>
// Change this if you want verbose debuging information in the console. // This will print a load of shit to the console
const debugMode = true; const debugMode = false;
const isGM4 = typeof GM_setValue === 'undefined'; const isGM4 = typeof GM_setValue === 'undefined';
const setValue = isGM4 ? GM.setValue : GM_setValue; const setValue = isGM4 ? GM.setValue : GM_setValue;
const getValue = isGM4 ? GM.getValue : GM_getValue; const getValue = isGM4 ? GM.getValue : GM_getValue;
const xmlHttpRequest = isGM4 ? GM.xmlHttpRequest : GM_xmlhttpRequest; const xmlHttpRequest = isGM4 ? GM.xmlHttpRequest : GM_xmlhttpRequest;
//
// DO NOT EDIT ANYTHING IN THIS SCRIPT DIRECTLY - YOUR FLAGS SHOULD BE CONFIGURED USING THE FLAG SELECT
//
const version = encodeURIComponent(2); // Breaking changes. const version = encodeURIComponent(2); // Breaking changes.
const back_end = 'https://flags.plum.moe/'; const back_end = 'https://flags.plum.moe/';
const api_flags = back_end + 'api/flags'; const api_flags = back_end + 'api/flags';
@ -55,181 +52,169 @@ let regions = []; // The flags we have selected.
let postNrs = []; // all post numbers in the thread. let postNrs = []; // all post numbers in the thread.
let board_id = ""; // The board we get flags for. let board_id = ""; // The board we get flags for.
let flagsLoaded = false; let flagsLoaded = false;
//
// DO NOT EDIT ANYTHING IN THIS SCRIPT DIRECTLY - YOUR FLAGS SHOULD BE CONFIGURED USING THE FLAG SELECT
//
const debug = text => { const debug = text => {
if (debugMode) if (debugMode)
console.log('[BantFlags] ' + text); console.log('[BantFlags] ' + text);
} }
// Test unqiue CSS paths to figure out what board software we're using. // Test unqiue CSS paths to figure out what board software we're using.
const software = { const software = {
yotsuba: window.location.host === 'boards.4chan.org', yotsuba: window.location.host === 'boards.4chan.org',
nodegucaDoushio: document.querySelector('b[id="sync"], span[id="sync"]') !== null, nodegucaDoushio: document.querySelector('b[id="sync"], span[id="sync"]') !== null,
foolfuuka: document.querySelector('div[id="main"] article header .post_data') !== null foolfuuka: document.querySelector('div[id="main"] article header .post_data') !== null
}; };
const createAndAssign = (element, source) => Object.assign(document.createElement(element), source); const makeElement = (tag, options) => Object.assign(document.createElement(tag), options);
const toggleFlagButton = state => document.getElementById('append_flag_button').disabled = state === 'off' ? true : false; const toggleFlagButton = state => document.getElementById('append_flag_button').disabled = state === 'off' ? true : false;
const flagSource = flag => flag_dir + flag + '.png';
const flagSource = flag => flag_dir + flag + ".png";
/** Add styles to the <head> */ /** Add styles to the <head> */
const addGlobalStyle = css => document.head.appendChild(createAndAssign('style', { innerHTML: css })); const addStyle = css => document.head.appendChild(makeElement('style', { innerHTML: css }));
/** Wrapper around GM_xmlhttpRequest.
* @param {string} method - The HTTP method (GET, POST).
* @param {string} url - The URL of the request.
* @param {string} data - text for the form body.
* @param {Function} func - The function run when we recieve a response. Response data is sent directly to it. */
const makeRequest = ((method, url, data, func) => { const makeRequest = ((method, url, data, func) => {
xmlHttpRequest({ xmlHttpRequest({
method: method, method: method,
url: url, url: url,
data: data, data: data,
headers: { "Content-Type": 'application/x-www-form-urlencoded' }, headers: { "Content-Type": 'application/x-www-form-urlencoded' },
onload: func onload: func
}); });
}); });
/** Itterate over selected flags are store them across browser sessions.*/ /** Itterate over selected flags are store them across browser sessions.*/
function saveFlags() { function saveFlags() {
regions = []; regions = [];
const selectedFlags = document.querySelectorAll("bantflags_flag"); const selectedFlags = document.querySelectorAll(".bantflags_flag");
for (var i = 0; i < selectedFlags.length; i++) { for (let i = 0; i < selectedFlags.length; i++) {
regions[i] = selectedFlags[i].title; regions[i] = selectedFlags[i].title;
} }
setValue(namespace, regions); setValue(namespace, regions);
} }
/** Add a flag to our selection. /** Add a flag to our selection. */
* @param {string} flag - The flag to add to our selection. Either passed from saved flags or the current value of flagSelect */
function setFlag(flag) { function setFlag(flag) {
let UID = Math.random().toString(36).substring(7); const flagName = flag ? flag : document.querySelector('#flagSelect input').value;
let flagName = flag ? flag : document.querySelector('#flagSelect input').value; const flagContainer = document.getElementById('bantflags_container');
let flagContainer = document.getElementById('bantflags_container');
flagContainer.appendChild(makeElement('img', {
flagContainer.appendChild(createAndAssign('img', { title: flagName,
title: flagName, src: flagSource(flagName),
src: flagSource(flagName), className: 'bantflags_flag',
id: UID, onclick: function() {
className: 'bantflags_flag' flagContainer.removeChild(this);
})); if (flagsLoaded)
toggleFlagButton('on');
if (flagContainer.children.length >= max_flags) saveFlags();
toggleFlagButton('off'); }
}));
document.getElementById(UID).addEventListener("click", e => {
flagContainer.removeChild(e.target); if (flagContainer.children.length >= max_flags)
toggleFlagButton('on'); toggleFlagButton('off');
saveFlags();
}); if (!flag) // We've added a new flag to our selection
saveFlags();
if (!flag) // We've added a new flag to our selection
saveFlags();
} }
function init() { function init() {
let flagsForm = createAndAssign('div', { const flagsForm = makeElement('div', {
className: 'flagsForm', className: 'flagsForm',
innerHTML: '<span id="bantflags_container"></span><button type="button" id="append_flag_button" title="Click to add selected flag to your flags. Click on flags to remove them. Saving happens automatically, you only need to refresh the pages that have an outdated flaglist on the page."><<</button><button id="flagLoad" type="button">Click to load flags.</button><div id="flagSelect" ><ul class="hide"></ul><input type="button" value="(You)" onclick=""></div>' innerHTML: '<span id="bantflags_container"></span><button type="button" id="append_flag_button" title="Click to add selected flag to your flags. Click on flags to remove them." disabled="true"><<</button><button id="flagLoad" type="button">Click to load flags.</button><div id="flagSelect" ><ul class="hide"></ul><input type="button" value="(You)" onclick=""></div>'
}); });
// Where do we append the flagsForm to? // Where do we append the flagsForm to?
if (software.yotsuba) { document.getElementById('delform').appendChild(flagsForm); } if (software.yotsuba) { document.getElementById('delform').appendChild(flagsForm); }
else if (software.nodegucaDoushio) { document.querySelector('section').append(flagsForm); } // As posts are added the flagForm moves up the page. Could we append this after .section? else if (software.nodegucaDoushio) { document.querySelector('section').insertAdjacentElement('afterEnd', flagsForm); }
for (let i = 0; i < regions.length; i++) { for (let i = 0; i < regions.length; i++) {
setFlag(regions[i]); setFlag(regions[i]);
} }
document.getElementById('append_flag_button').addEventListener('click', () => flagsLoaded ? setFlag() : alert('Load flags before adding them.')); document.getElementById('flagLoad').addEventListener('click', makeFlagSelect, { once: true });
document.getElementById('flagLoad').addEventListener('click', makeFlagSelect, { once: true });
} }
/** Get flag data from server and fill flags form. */ /** Get flag data from server and fill flags form. */
function makeFlagSelect() { function makeFlagSelect() {
makeRequest( makeRequest(
"GET", "GET",
api_flags, api_flags,
"", // We can't send data, it's a GET request. "", // We can't send data, it's a GET request.
function (resp) { function (resp) {
debug('Loading flags.'); debug('Loading flags.');
if (resp.status !== 200) { if (resp.status !== 200) {
console.log('Couldn\'t get flag list from server') console.log('Couldn\'t get flag list from server')
return; return;
} }
let flagSelect = document.getElementById('flagSelect'); let flagSelect = document.getElementById('flagSelect');
let flagList = flagSelect.querySelector('ul'); let flagInput = flagSelect.querySelector('input');
let flagInput = flagSelect.querySelector('input'); let flagList = flagSelect.querySelector('ul');
let flags = resp.responseText.split('\n');
let flags = resp.responseText.split('\n');
for (var i = 0; i < flags.length; i++) { for (let i = 0; i < flags.length; i++) {
let flag = flags[i]; const flag = flags[i];
flagList.appendChild(createAndAssign('li',{ flagList.appendChild(makeElement('li',{
innerHTML: `<img src="${flagSource(flag)}" title="${flag}"><span>${flag}</span>` innerHTML: `<img src="${flagSource(flag)}" title="${flag}"><span>${flag}</span>`
})); }));
} }
flagSelect.addEventListener('click', function (e) { flagSelect.addEventListener('click', e => {
// So it works if we click the flag image // Maybe we clicked the flag image
const node = e.target.nodeName === 'LI' ? e.target : e.target.parentNode; const node = e.target.nodeName === 'LI' ? e.target : e.target.parentNode;
if (node.nodeName === 'LI') { if (node.nodeName === 'LI')
flagInput.value = node.querySelector('span').innerHTML; flagInput.value = node.querySelector('span').innerHTML;
}
flagList.classList.toggle('hide'); flagList.classList.toggle('hide');
}); });
document.getElementById('flagLoad').style.display = 'none'; const flagButton = document.getElementById('append_flag_button');
document.querySelector('.flagsForm').style.marginRight = "200px"; // Element has position: absolute and is ~200px long. flagButton.addEventListener('click', () => setFlag());
flagSelect.style.display = 'inline-block'; flagButton.disabled = false;
flagsLoaded = true;
}); document.getElementById('flagLoad').style.display = 'none';
document.querySelector('.flagsForm').style.marginRight = "200px"; // flagsForm has position: absolute and is ~200px long.
flagSelect.style.display = 'inline-block';
flagsLoaded = true;
});
} }
/** add all of the post numbers on the page to postNrs. */ /** add all of the post numbers on the page to postNrs. */
function getPosts(selector) { function getPosts(selector) {
const posts = document.querySelectorAll(selector); const posts = document.querySelectorAll(selector);
for (let i = 0; i < posts.length; i++) { for (let i = 0; i < posts.length; i++) {
const postNumber = software.yotsuba const postNumber = software.yotsuba
? posts[i].id.substr(2) // Fuck you 4chan. ? posts[i].id.substr(2) // Fuck you 4chan.
: posts[i].id; : posts[i].id;
postNrs.push(postNumber); postNrs.push(postNumber);
} }
debug(postNrs); debug(postNrs);
} }
/** Get flags from the database using values in postNrs and pass the response on to onFlagsLoad */ /** Get flags from the database using values in postNrs and pass the response on to onFlagsLoad */
function resolveFlags() { function resolveFlags() {
makeRequest( makeRequest(
'POST', 'POST',
api_get, api_get,
'post_nrs=' + encodeURIComponent(postNrs) + '&board=' + encodeURIComponent(board_id) + '&version=' + version, 'post_nrs=' + encodeURIComponent(postNrs) + '&board=' + encodeURIComponent(board_id) + '&version=' + version,
function (resp) { function (resp) {
if (resp.status !== 200) {
if (resp.status !== 200) { console.log('[bantflags] Couldn\'t load flags. Refresh the page');
console.log('[bantflags] Couldn\'t load flags. Refresh the page.'); debug(resp.responseText);
return; return;
} }
const jsonData = JSON.parse(resp.responseText); const jsonData = JSON.parse(resp.responseText);
debug(`JSON: ${resp.responseText}`); debug(`JSON: ${resp.responseText}`);
Object.keys(jsonData).forEach(post => { Object.keys(jsonData).forEach(post => {
let flags = jsonData[post]; const flags = jsonData[post];
if (flags.length <= 0) if (flags.length <= 0) return;
return;
debug(`Resolving flags for >>${post}`); debug(`Resolving flags for >>${post}`);
@ -240,156 +225,145 @@ function resolveFlags() {
for (let i = 0; i < flags.length; i++) { for (let i = 0; i < flags.length; i++) {
const flag = flags[i]; const flag = flags[i];
const newFlag = createAndAssign('a', { flagContainer.append(makeElement('a', {
innerHTML: `<img src="${flagSource(flag)}" title="${flag}">`, innerHTML: `<img src="${flagSource(flag)}" title="${flag}">`,
className: 'bantFlag', className: 'bantFlag',
target: '_blank', target: '_blank',
title: flag title: flag
}); }));
flagContainer.append(newFlag);
debug(`\t -> ${flag}`); debug(`\t -> ${flag}`);
} }
}); });
postNrs = []; postNrs = [];
}); });
} }
function main() { function main() {
if (!regions) { // Should only be called before you set flags for the first time. if (!regions) {
regions = []; regions = [];
window.confirm('[BantFlags]: No Flags detected.\nIf this is your first time running bantflags, look for the "Click to load flags." button at the bottom right of the thread, then select your flag and press the ">>" button.'); }
}
// See Docs/styles.css
// See Docs/styles.css addStyle('.bantFlag{padding: 0px 0px 0px 5px; display: inline-block; width: 16px; height: 11px; position: relative;} .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;}}.flagsForm{float: right; clear: right; margin: 0 20px 10px 0;} #flagSelect{display:none;} #flagSelect ul{list-style-type: none;padding: 0;margin-bottom: 0;cursor: pointer;bottom: 100%;height: 200px;overflow: auto;position: absolute;width:200px;background-color:#fff} #flagSelect ul li {display: block;} #flagSelect ul li:hover {background-color: #ddd;}#flagSelect {position: absolute;}#flagSelect input {width: 200px;} #flagSelect .hide {display: none;}#flagSelect img {margin-left: 2px;}')
addGlobalStyle('.bantFlag{padding: 0px 0px 0px 5px; display: inline-block; width: 16px; height: 11px; position: relative;} .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;}}.flagsForm{float: right; clear: right; margin: 20px 10px;} #flagSelect{display:none;} #flagSelect ul{list-style-type: none;padding: 0;margin-bottom: 0;cursor: pointer;bottom: 100%;height: 200px;overflow: auto;position: absolute;width:200px;background-color:#fff} #flagSelect ul li {display: block;} #flagSelect ul li:hover {background-color: #ddd;}#flagSelect {position: absolute;}#flagSelect input {width: 200px;} #flagSelect .hide {display: none;}#flagSelect img {margin-left: 2px;}')
if (software.yotsuba) { if (software.yotsuba) {
getPosts('.postContainer'); getPosts('.postContainer');
addGlobalStyle('.flag{top: 0px;left: -1px}'); addStyle('.flag{top: 0px;left: -1px}');
init(); init();
} }
if (software.nodegucaDoushio) { else if (software.nodegucaDoushio) {
getPosts('section[id], article[id]'); getPosts('section[id], article[id]');
addGlobalStyle('.bantFlag {cursor: default} .bantFlag img {pointer-events: none;}'); addStyle('.bantFlag {cursor: default} .bantFlag img {pointer-events: none;}');
init(); init();
} }
if (software.foolfuuka) { else if (software.foolfuuka) {
getPosts('article[id]'); getPosts('article[id]');
addGlobalStyle('.bantFlag{top: -2px !important;left: -1px !important}'); addStyle('.bantFlag{top: -2px !important;left: -1px !important}');
} }
board_id = window.location.pathname.split('/')[1]; board_id = window.location.pathname.split('/')[1];
debug(board_id); debug(`board: ${board_id}`);
resolveFlags(); try {
resolveFlags();
}
catch (fuckywucky) {
console.log(`Wah! Manx fucked something up ;~;\nPoke him somewhere with this:\n${fuckywucky}`)
}
} }
if (isGM4) { // Fuck you GM4 if (isGM4) { // Fuck you GreaseMonkey
(async () => { (async () => {
regions = await getValue(namespace); regions = await getValue(namespace);
main(); main();
})(); })();
} }
else { else {
regions = getValue(namespace); regions = getValue(namespace);
main(); main();
} }
const postFlags = (post_nr, func = resp => debug(resp.responseText)) => makeRequest( const postFlags = (post_nr, func = resp => debug(resp.responseText)) => makeRequest(
'POST', 'POST',
api_post, api_post,
`post_nr=${encodeURIComponent(post_nr)}&board=${encodeURIComponent(board_id)}&regions=${encodeURIComponent(regions)}&version=${version}`, `post_nr=${encodeURIComponent(post_nr)}&board=${encodeURIComponent(board_id)}&regions=${encodeURIComponent(regions)}&version=${version}`,
func); func);
if (software.yotsuba) { if (software.yotsuba) {
const GetEvDetail = e => e.detail || e.wrappedJSObject.detail; const e_detail = e => e.detail || e.wrappedJSObject.detail// what?
document.addEventListener('QRPostSuccessful', e => postFlags(e_detail(e).postID));
// 4chanX and native extension respectively document.addEventListener('4chanQRPostSuccess', e => postFlags(e_detail(e).postId));
document.addEventListener('QRPostSuccessful', e => postFlags(e.detail.postID));
document.addEventListener('4chanQRPostSuccess', e => postFlags(GetEvDetail(e).postId)); document.addEventListener('ThreadUpdate', e => {
const d = e_detail(e)
// I need to look at these. if (d[404]) return;
document.addEventListener('ThreadUpdate', function (e) {
var evDetail = GetEvDetail(e); d.newPosts.forEach(post => postNrs.push(post.split('.')[1]));
var evDetailClone = typeof cloneInto === 'function' ? cloneInto(evDetail, unsafeWindow) : evDetail;
resolveFlags();
//ignore if 404 event });
if (evDetail[404] === true) {
return; document.addEventListener('4chanThreadUpdated', e => {
} const d = e_detail(e);
if (d.count <= 0) return;
evDetailClone.newPosts.forEach(function (post_board_nr) {
var post_nr = post_board_nr.split('.')[1]; // Get the added posts in reverse order, take post numbers from ID
postNrs.push(post_nr); const posts = document.querySelectorAll('.postContainer');
}); for (let i = 0; i < d.count; i++) {
postNrs.push(posts[posts.length - 1 - i].id.substr(2));
resolveFlags(); }
}, false);
resolveFlags();
document.addEventListener('4chanThreadUpdated', function (e) { });
var evDetail = GetEvDetail(e);
let threadID = window.location.pathname.split('/')[3];
let postsContainer = Array.prototype.slice.call(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)
lastPosts.forEach(function (post_container) {
var post_nr = post_container.id.replace('pc', '');
postNrs.push(post_nr);
});
resolveFlags();
}, false);
} }
if (software.nodegucaDoushio) { if (software.nodegucaDoushio) {
const postFunc = () => {
const postFunc = function() {
postNrs.push(mutation.target.id); postNrs.push(mutation.target.id);
resolveFlags(); resolveFlags();
} }
const badNodes = ['HR', 'SECTION']; const badNodes = ['HR', 'SECTION'];
new MutationObserver(mutations => { new MutationObserver(mutations => {
mutations.forEach(mutation => { mutations.forEach(mutation => {
if (mutation.addedNodes.length <= 0) if (mutation.addedNodes.length <= 0)
return; // We only care if something post was added return; // We only care if something post was added
var firstAddedNode = mutation.addedNodes[0].nodeName; const firstAddedNode = mutation.addedNodes[0].nodeName;
// Enter a thread / change boards // Enter a thread / change boards
if (mutation.target.nodeName === 'THREADS') { if (mutation.target.nodeName === 'THREADS') {
if (badNodes.includes(firstAddedNode)) if (badNodes.includes(firstAddedNode))
return; // We are in the index and a post was added, handled properly further down return; // We are in the index and a post was added, handled properly further down
board_id = window.location.pathname.split('/')[1]; board_id = window.location.pathname.split('/')[1];
setTimeout(getPosts('section[id], article[id]'), 2000); setTimeout(getPosts('section[id], article[id]'), 2000);
resolveFlags(); resolveFlags();
init(); init();
} }
// We post // We post
if (firstAddedNode === 'HEADER') { if (firstAddedNode === 'HEADER') {
postFlags(mutation.target.id, postFunc) postFlags(mutation.target.id, postFunc)
} }
// Someone else posts // Someone else posts
if (firstAddedNode === 'ARTICLE') { if (firstAddedNode === 'ARTICLE') {
if (mutation.target.nodeName === 'BODY' || mutation.target.id === 'hover_overlay') if (mutation.target.nodeName === 'BODY' || mutation.target.id === 'hover_overlay')
return; // User is hovering over a post return; // User is hovering over a post
postNrs.push(mutation.addedNodes[0].id); postNrs.push(mutation.addedNodes[0].id);
setTimeout(resolveFlags, 1500); setTimeout(resolveFlags, 1500);
} }
}); });
}).observe(document.body, { childList: true, subtree: true }); }).observe(document.body, { childList: true, subtree: true });
} }

@ -1,16 +1,16 @@
[Unit] [Unit]
Description=bantflags backend Description=bantflags serb
[Service] [Service]
# Set to the location of the application # Set to the location of the application
WorkingDirectory=/etc/bantflags WorkingDirectory=/var/www/bantflags/src/
ExecStart=/usr/bin/dotnet /etc/bantflags/BantFlags.dll ExecStart=/usr/bin/env sbcl --load init.el
Restart=always Restart=always
# restarts 10 seconds after it goes bang # restarts 10 seconds after it goes bang
RestartSec=10 RestartSec=10
KillSignal=SIGINT KillSignal=SIGINT
SyslogIdentifier=bantflags SyslogIdentifier=bantflags
User=www-data User=nginx
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

36
env/database.sql vendored

@ -0,0 +1,36 @@
DROP DATABASE IF EXISTS `bantflags`;
CREATE DATABASE `bantflags`;
CREATE USER IF NOT EXISTS flags@localhost IDENTIFIED BY 'default';
GRANT ALL PRIVILEGES ON bantflags.* TO flags@localhost;
FLUSH PRIVILEGES;
USE `bantflags`;
CREATE TABLE `flags` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`flag` varchar(100) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `flag` (`flag`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
CREATE TABLE `posts` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`post_nr` int(10) NOT NULL DEFAULT '0',
`board` varchar(10) NOT NULL DEFAULT 'bant',
PRIMARY KEY (`id`),
UNIQUE KEY `post_nr_board` (`post_nr`,`board`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
CREATE TABLE `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`),
KEY `flag` (`flag`),
KEY `post_nr` (`post_nr`),
CONSTRAINT `flag` FOREIGN KEY (`flag`) REFERENCES `flags` (`id`) ON DELETE CASCADE,
CONSTRAINT `post_nr` FOREIGN KEY (`post_nr`) REFERENCES `posts` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
INSERT INTO `flags` (`flag`) VALUES ('empty, or there were errors. Re-set your flags.');

@ -15,7 +15,7 @@ http {
sendfile on; sendfile on;
tcp_nopush on; tcp_nopush on;
charset utf-8; charset utf-8;
index index.htm index.html; index index.html;
server { server {
root /var/www/flags; root /var/www/flags;

@ -0,0 +1,23 @@
;; (C) Copyright 2020 C-xC-c <boku@plum.moe>
;; This file is part of bantflags.
;; bantflags is licensed under the GNU AGPL Version 3.0 or later.
;; see the LICENSE file or <https://www.gnu.org/licenses/>
(asdf:defsystem #:bantflags
:description "the bantflags server component"
:author "Manx (boku@plum.moe)"
:mailto "boku@plum.moe"
:license "AGPLv3+"
:version "0.0.1"
:serial t
:depends-on (:hunchentoot
:hunchenhelpers
:cl-ppcre
:clsql
:jonathan)
:Components
((:file "package")
(:file "utils")
(:file "db")
(:file "config")
(:file "main")))

@ -0,0 +1,14 @@
(in-package #:bantflags)
(defvar config
'((boards ("bant" "uhh"))
(staging-password "not implemented")
(db-conn ("localhost" "bantflags" "flags" "default"))
(poolsize 3)
(www-root #p"/path/to/files/")
(port 4242)
;; These can be a file or stream, make them nil to disable logging
;; If the file can't be accessed, throws a weird error. See
;; README.org
(access-log *standard-output*)
(error-log #p"/path/to/error/log/")))

@ -0,0 +1,47 @@
;; (C) Copyright 2020 C-xC-c <boku@plum.moe>
;; This file is part of bantflags.
;; bantflags is licensed under the GNU AGPL Version 3.0 or later.
;; see the LICENSE file or <https://www.gnu.org/licenses/>
(in-package #:bantflags)
;; Databases in common lisp are the fucking worst.
;; Don't even bother.
;; We're comparing strings
(defparameter *flags* (make-hash-table :test 'equal))
(defparameter *boards* (make-hash-table :test 'equal))
(defparameter *flags-txt* nil)
(defparameter conn nil)
(defparameter get-posts-sql "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 posts.post_nr in (~{'~a'~^,~}) and posts.board = '~a';")
(defmacro dbfun (name &rest body)
`(defun ,name ,(car body)
(clsql:with-database (db conn :database-type :mysql :pool t)
,@(cdr body))))
(defun flag-id (flag)
(gethash flag *flags*))
(dbfun insert-post (post_nr board flags)
(clsql:execute-command (format nil "insert ignore into posts (post_nr, board) values (~a, '~a');" post_nr board) :database db)
(let ((post-id (caar (clsql:query (format nil "select id from posts where post_nr = ~a and board = '~a';" post_nr board) :database db))))
(clsql:execute-command
(with-output-to-string (s)
(format s "insert into postflags (post_nr, flag) values")
(loop for flag in (butlast flags) ;; The last flag needs a semi-colon instead of a comma
do (format s "(~a,~a)," post-id (flag-id flag)))
(format s "(~a,~a);" post-id (flag-id (car (last flags)))))
:database db)))
(dbfun get-posts (posts board)
(let ((result (clsql:query (format nil get-posts-sql posts board) :database db))
(table (make-hash-table)))
(loop for (post_nr . flag) in (reverse result) do
(unless (gethash post_nr table)
(setf (gethash post_nr table) '()))
(push (car flag) (gethash post_nr table)))
(jojo:to-json table)))
(dbfun get-flags ()
(clsql:query "select flags.id, flags.flag from flags" :database db))

@ -0,0 +1,9 @@
;; This needs to be run with a lisp started in the same directory as
;; bantflags
(push (truename ".") ql:*local-project-directories*)
(ql:register-local-projects)
(ql:quickload :bantflags)
(bantflags:main)
(hunchentoot:start bantflags:*serb*)
(loop (sleep 43200) (gc :full t))

@ -0,0 +1,53 @@
;; (C) Copyright 2020 C-xC-c <boku@plum.moe>
;; This file is part of bantflags.
;; bantflags is licensed under the GNU AGPL Version 3.0 or later.
;; see the LICENSE file or <https://www.gnu.org/licenses/>
(in-package :bantflags)
(defun init ()
(assert (not (null config)))
(setf conn (conf 'db-conn))
(loop repeat (conf 'poolsize)
do ;; This doesn't work lole
(clsql:connect conn :database-type :mysql :pool t :if-exists :new))
(set-boards)
(set-flags)
(defvar *serb* (make-instance 'hunchentoot:easy-acceptor
:port (conf 'port)
:address "127.0.0.1" ;; localhost
:document-root (conf 'www-root)
:access-log-destination (conf 'access-log)
:message-log-destination (conf 'error-log))))
(defun main ()
(handler-case (init)
(error (c)
(format t "Init fucked up, exiting ~a" c)
(return-from main)))
(handler-case (hunchentoot:start *serb*)
(error (c)
(format t "couldn't start serb: ~a" c)
(return-from main))))
(defmethod hunchentoot:acceptor-status-message (acceptor (http-status-code (eql 404)) &key)
(format nil "")) ;; Empty 404 page
(henh:handle :post (api-post :uri "/api/post") @json
(post_nr regions board version)
(multiple-value-bind (result msg) (insert-post-p post_nr (cl-ppcre:split "," regions) board)
(cond
(result
(insert-post post_nr board msg)
(format nil "{\"~a\": [~{\"~a\"~^,~}]}~%" post_nr msg)) ;; This makes JSON
(t (format nil "{\"Error\": \"~a\"}~%" msg)))))
(henh:handle :post (api-get :uri "/api/get") @json
(post_nrs board version)
(setf post_nrs (cl-ppcre:split "," post_nrs))
(if (get-posts-p post_nrs board)
(format nil "~a~%" (get-posts post_nrs board))
(t (format nil "{[\"~a\"]}~%" "bad"))))
(henh:handle :get (api-flags :uri "/api/flags") @plain
()
(format nil "~a~%" *flags-txt*))

@ -0,0 +1,4 @@
(defpackage #:bantflags
(:use #:cl)
(:export :init
:*serb*))

@ -0,0 +1,59 @@
;; (C) Copyright 2020 C-xC-c <boku@plum.moe>
;; This file is part of bantflags.
;; bantflags is licensed under the GNU AGPL Version 3.0 or later.
;; see the LICENSE file or <https://www.gnu.org/licenses/>
(in-package #:bantflags)
(defvar empty-flag '("empty, or there were errors. Re-set your flags."))
(defun conf (thing &aux (item (cadr (assoc thing config))))
(if (null item)
(error "no such config item" thing)
item))
(defun set-boards ()
(setf *boards* (make-hash-table :test 'equal))
(mapc (lambda (board) (setf (gethash board *boards*) t)) (conf 'boards)))
(defun set-flags ()
(setf *flags* (make-hash-table :test 'equal))
(let ((flags (get-flags)))
(loop for (id . flag) in flags
do (setf (gethash (car flag) *flags*) id))
(setf *flags-txt* ;; We don't want users to select `empty-flag`
(cl-ppcre:regex-replace (concatenate 'string (car empty-flag) "\\n") ;; newline
(format nil "~{~a~^~%~}" (mapcan (lambda (x) (cdr x)) flags))
""))))
;; validation
(defun post-number-p (post_nr)
(if (or (null post_nr)
(null (parse-integer post_nr :junk-allowed t)))
nil
post_nr))
(defun boardp (board)
(gethash board *boards*))
(defun insert-post-p (post_nr regions board)
(cond
((not (post-number-p post_nr))
(values nil "Invalid post number."))
((not (boardp board))
(values nil "Invalid board parameter."))
((null regions)
(values t empty-flag))
((< 30 (length regions))
(values nil "Too many flags."))
((every (lambda (flag) (gethash flag *flags*)) regions)
(values t regions))
(t (values t empty-flag))))
(defun get-posts-p (post_nrs board)
(and (not (null post_nrs))
(every #'post-number-p post_nrs)
(boardp board)))
;; Content types
(defparameter @json "application/json")
(defparameter @plain "text/plain")
Loading…
Cancel
Save